Tuesday, February 07, 2012   
 Search   
 
 Access and Assign events in Visual FoxPro    

FoxTalk.gif

Issue Date: FoxTalk 2.0 November 2006

Streamline configuration by assigning global options with Access and Assign events in Visual FoxPro

If your organization or clients have multiple instances of an application running on a network, they may find it tedious to manage the various configuration options. With our Global Options object, your application can store configuration options in one place, making life much easier for administrators and users.

To help make it easier for your application to handle configuration options, we'll:

  • Show how to store and retrieve global application settings with a class that implements our Global Options object.
  • Create a way to define storage classes that can employ different storage approaches, using an abstract class.
  • Demonstrate how to use one of our storage classes in a real-world setting involving storage in an INI file.
When you need an elegant, flexible way to alter the way a program functions in midstream, you may want to consider using events. In a previous article, Global Access and Assign Events, we demonstrated a generic class that triggers events whenever code reads (i.e., accesses) or writes to (i.e., assigns a value to) any public property of the class. Now, we'll show you how to leverage these features to make a class that's useful in a real application. A very good candidate for such a real-life example is a class that implements a Global Options object for a program. This class stores all global settings, which you can share across all instances of the program running on any computer in the network.

A Global Options object

The Global Options class inherits from the bzAccessAssign class (described in the previously mentioned article), which achieves the basic concept of a global handler for ACCESS and ASSIGN methods. Besides this functionality, the Global Options class must be able to save and load the values for its properties (that is, settings for the program) to persistent storage (i.e., a file on disk) so all instances of the program running on any computer in the local network will have access to global settings.
We create two new classes to implement this functionality:
  • bzOptions - which inherits from bzAccessAssign and serves as the Global Options class
  • bzOptionsStorage_Base - which the Global Options class uses as an abstract storage mechanism
Figure A shows the structure of bzOptions, and Figure B the bzOptionsStorage_Base abstract class.
 
Figure A: The bzOptions class inherits from the bzAccessAssign class and has three new protected properties to connect with the storage class.
 
ft206b1a.gif
 
Figure B: bzOptionaStorage_base is an abstract class; it defines the methods that must be implemented by a real storage class.
 

ft206b1b.gif

Global Options class

Class bzOptions doesn't have any special methods. It just has three private properties. The properties are set as Private to avoid triggering PropertyAccess and PropertyAssign events. The properties are:
 
  • __cStorageClass and __cStorageClassLib. The bzOptions class uses them to store the name of the class and the name of appropriate classlib from which to create the storage object. If __cStorageClassLib is empty, the code creates the storage object using the CreateObject function, and the name of the library must be in the classlib SET variable. If both __cStorageClass and __cStorageClassLib are not empty, the code creates the storage object using the NewObject function.
  • __oStorageObject. The Global Options object stores in this property the reference to the storage object.
The class used to create the storage object must be inherited from the bzOptionsStorage_Base class. The bzOptions class relies on the public interface of bzOptionsStorage_base to handle storing and retrieving the values of the properties from persistent storage.
 
The bzOptions class creates the storage object in its Init event. If the storage class can't be created for any reason, or if it doesn't expose the methods of the bzOptionsStorage_base (which means it isn't created from a class inherited from the bzOptionsStorage_base class), the bzOptions class's Init method returns .F., and the bzOptions class isn't created either. If the storage object is properly created and has the required methods to implement the storage interface, the bzOptions class calls the storage object's RecreateOptionsObject method. Listing A shows the bzOptions class Init method. We'll discuss the RecreateOptionsObject method along with the bzOptionsStorage_base class later.
 
Listing A: Code for the bzOptions class Init method
 
*-- bzOptions.Init
IF NOT DODEFAULT()
 RETURN .f.
ENDIF
LOCAL loStorage
DO CASE
   CASE NOT EMPTY(this.__cstorageclass) AND ; 
      EMPTY(this.__cstorageclasslib) 
  
      loStorage = CREATEOBJECT(this.__cstorageclass) 
   
   CASE NOT EMPTY(this.__cstorageclass) AND ; 
      NOT EMPTY(this.__cstorageclasslib) 
  
      loStorage = NEWOBJECT(this.__cstorageclass, this.__cstorageclasslib)
ENDCASE
lSuccess = (VARTYPE(loStorage)="O")
IF lSuccess AND ; 
   PEMSTATUS(loStorage, "RecreateOptionsObject", 5) AND ; 
   PEMSTATUS(loStorage, "LoadSetting", 5) AND ; 
   PEMSTATUS(loStorage, "SaveSetting", 5) THEN 
   
   lSuccess = loStorage.RecreateOptionsObject(this)
ELSE 
   lSuccess = .f.
ENDIF
IF lSuccess then 
   this.__ostorageobject = loStorage
ENDIF
RETURN lSuccess

Class bzOptions uses the PropertyAccess and PropertyAssign events to know when to save or restore the values of accessed and/or changed properties. The code from the PropertyAccess and PropertyAssign events simply calls the LoadSetting and SaveSetting of the storage class. Listing B shows the code for the PropertyAccess and PropertyAssign methods of the bzOptions class.
 
Listing B: Code for PropertyAccess and PropertyAssign methods for bzOptions class
 
*-- bzOptions.PropertyAccess event
LPARAMETERS cMember, cDataType
this.&cMember. = this.__oStorageObject.LoadSetting(cMember, cDataType)

*-- bzOptions.PropertyAssign event
LPARAMETERS cMember, mNewValue
this.__ostorageobject.SaveSetting(cMember, mNewValue)

Abstract Storage class for Global Options object

The bzOptionsStorage_Base class is responsible for saving and loading values from persistent storage and is instantiated as a member of the Global Options class. It's defined as an abstract class. That is, it doesn't serve directly as any storage approach; rather, it serves as just the interface, which must be implemented by a derived storage class which provides the specific storage type. This approach allows the program to easily use different storage mechanisms without changing the logic of the Global Options class (so it's possible to have the storage mechanism be a table in a database, an XML file, or even something a little more unusual such as a web service over the internet).
 
Class bzOptionsStorage_Base is defined as an abstract class, so it doesn't implement any code. It defines the interface used by any specific storage class and employs the following methods:
 
  • InitStorage (Protected). The storage class calls this method from the Init event. It's used to initialize the storage. If storage is a table, InitStorage can create a private data session, open the storage table, and eventually update the structure of the storage table automatically if there are new settings (properties) in the Options class.
  • CloseStorage (Protected). The storage class calls this method from the Destroy event. It's responsible for all required cleanup code. If storage is a table opened in a private data session, the CloseStorage method will close options table and release the data session.
  • LoadSetting (Public). The Global Options object calls this method directly to load the value of a property from the storage file. It receives two parameters: cMember (which signifies a property name of the Global Options object that needs to be loaded from storage) and cDataType (which signifies that property�s data type, as returned by the VARTYPE function). LoadSettings must convert the value read from the storage to the data type specified in the cDataType parameter and then return the result to the Global Options object. For example, if the storage file is a text file, and the property saved has the DATE type, then when the value is read from file, it must be converted to the proper data type expected by the program (DATE in this example).
  • SaveSetting (Public). The Global Options object calls this method to save the value of a property to the storage file, when the property was changed. It receives two parameters: cMember (the name of the property which was changed by the program in the Global Options object) and mNewValue (for the new value of the property).
  • RecreateOptionsObject (Public). This method receives a single parameter, which is a reference to the Options object. It offers a very powerful feature: It acts as a hook, and using it the Global Options object will automatically re-create properties added dynamically at runtime, with data saved into the storage file.
Note: To better understand why the Global Options object must re-create properties at this time, consult the side box Plugin architecture

Implementing a real-life Storage Class

To be able to actually implement a storage class, we need to create a class that's inherited from bzOptionsStorage_Base and that will implement the methods defined in the base storage class. To provide a userful example, we created the class bzOptionsStorage_INI, which allows you to store settings in an INI file.
 
INI files are still a good option to store global application options. They have an advantage over registry settings because you can store them on a server in a shared folder, making them accessible to all instances of the program running across the local network or intranet. To perform read and write access to the INI file, we use the oldIniReg class, as defined in the registry.vcx library (part of FoxPro Foundation Classes - FFC). We defined a custom class, bzRegistry, which simply inherits oldIniReg.
 
Class bzOptionsStorage_INI has the following properties:
  • cIniRegistryClass and cIniRegistryClassLib (Public). Contains the name of the class (and the library) used to read/write INI files. Default values are bzRegistry and bz_Registry.VCX, which points to the custom registry class inherited from the oldIniReg FFC class.
  • cINIFile (Public). Contains the name of the INI file. Default value is APPOPTIONS.INI. At runtime, this property will store the full qualified path of the INI file located in the root folder of the program. This can be changed by creating a new class inherited from bzOptionsStorage_INI and redefining the InitStorage method.
  • cSectionName (Public). Contains the name of the section in the INI file where the settings will be stored (all INI files must have at least one section).
  • oINI (Hidden). Stores a reference to the object used to read/write INI files. Its purpose it to prevent the INI handling object from being created every time when a setting is read or written.
Class bzOptionsStorage_INI redefines all abstract methods of its parent class to implement access to INI files. The InitStorage method, shown in Listing C, prepares the full filename of the INI file, initializes the INI section name, and creates the object used to read/write to the INI file (error-handling code was removed for simplicity).
 
Listing C: Code for the InitStorage method, which creates the INI file used to store program options
*-- bzOptionsStorage_INI.InitStorage
*-- prepare the full path of ini file
IF EMPTY(this.cinifile)
this.cinifile = "APPOPTIONS.INI"
ENDIF
*-- if not path is set,use application's folder
IF EMPTY(JUSTPATH(this.cinifile))
*-- if start from an exe, set the exe path
*-- else use current dir
IF UPPER(JUSTEXT(SYS(16)))="EXE"
 lcPath = SYS(5)   JUSTPATH(SYS(16))
ELSE�
 lcPath = SYS(5)   SYS(2003)
ENDIF
this.cinifile = ADDBS(lcpath)   this.cinifile
ENDIF
*-- init section name
IF EMPTY(this.csectionname)
this.csectionname = "Global Options"
ENDIF�
*-- create INI reader object
IF EMPTY(this.ciniregistryclasslib)
this.oini = CREATEOBJECT(this.ciniregistryclass)
ELSE
this.oini = NEWOBJECT(this.ciniregistryclass, ;
 this.ciniregistryclasslib)
endif
lSuccess = (VARTYPE(this.oini)="O")
RETURN lSuccess

 
The CloseStorage method releases the oINI object. The SaveSetting method writes an entry to the INI file with the name of the property being changed and the value of the property. The value is converted to string, to be stored in an INI file, using TRANSFORM function. Listing D shows the code for the SaveSetting method.
 
Listing D: Code for SaveSetting method
LPARAMETERS cSettingName, mNewValue
this.oini.writeinientry(TRANSFORM(mNewValue), this.csectionname, cSettingName, this.cinifile)
 
LoadSetting receives two parameters: the property name for which to load data, and the expected data type. The method reads the value of the property from the INI file. The value read from INI is always of type string, and the method attempts to convert it to the proper type expected by the property itself. It uses the data type of the property, passed in the second parameter.
 
As long as the data type of the property wasn't changed since its value was saved previously, the value can be converted back from string to the proper type using the CAST function. But there's a particular situation that occurs if a new property is added to an object in FoxPro - either at runtime or at design time - without a specified default value. In this case, since there's no default value, FoxPro automatically assigns the initial value as .F., which makes the initial data type of type Logic (L). Later, the property receives the proper value, so it changes its type to a different data type. For such properties, when the Options object is created, it's possible the several properties are initialized as .F. When they're read, the data type passed to the LoadSetting method is Logic (L). In this case, the code might attempt to convert the actual value of the property from the INI (which can actually be DATE, NUMBER, DATETIME, etc.) file to Logic, which will cause a wrong value to be returned from the LoadSetting method. To handle this situation, if cDataType parameter is Logic, LoadSetting method will try to guess the correct data type based on the actual value stored in INI file. To accomplish this, it will perform a double conversion by converting the value first to a data type, the result back to string, and then comparing the final result with the value read from INI. Listing E shows the code for the LoadSetting method.
 
Listing E: Code for LoadSettings method
LPARAMETERS cSettingName, cDataType
local lcVal, mRetVal
*-- cDataType parameter is optional,�
*-- if not passed I assume it to be logic
cDataType = IIF(VARTYPE(cDataType)="C", cDataType, "L")
IF NOT INLIST(cDataType, "N", "C", "D", "T", "L")
 cDataType="L"
ENDIF

lcVal=""
*-- read value from INI file
this.oIni.getinientry(@lcVal, this.csectionname, ;
cSettingName, this.cinifile)
*-- now convert to required data type
*-- no conversion is required for char
*-- if datatype match, no conversion,�
*-- otherwise use CAST to convert to target type
DO CASE
CASE cDataType = VARTYPE(lcVal)
 mRetVal = lcVal
CASE cDataType ="L"�
 *-- if property type is L, it might be because
 *-- the property was not initialized yet
 *-- so read the value from INI and try to match�
 *-- the correct type by using CAST and TRANSFORM
 DO CASE�
  *-- test if logic
  CASE TRANSFORM(CAST(lcVal as L)) == lcVal
   mRetVal = CAST(lcVal as L)
  *-- test if datetime
  CASE TRANSFORM(CAST(lcVal as T)) == lcVal
   mRetVal = CAST(lcVal as T)
  *-- test if date
  CASE TRANSFORM(CAST(lcVal as D)) == lcVal
   mRetVal = CAST(lcVal as D)
  *-- test if number
  CASE TRANSFORM(val(lcVal)) == lcVal
   mRetVal = val(lcVal)
  OTHERWISE
   *-- otherwise, consider string
   mRetVal = lcVal
 ENDCASE
OTHERWISE�
 mRetVal = CAST(lcVal as &cDataType.)
ENDCASE�
RETURN mRetVal

 
Method RecreateOptionsObject is used to automatically re-create properties added to the Options object at runtime based on the data from the INI file. It receives a single parameter, a reference to the Options object. All settings (entries) from the INI file correspond to properties of the Options object. So, if there are some entries in the INI file, but the Options object doesn't have corresponding properties, it means those properties were added to Options object at runtime, so they must be created again. The code read the whole section from the INI file, and for each entry, checks the Options object for the corresponding property. If not found, it's added. Listing F shows the code for method RecreateOptionsObject.
 
Listing F: Code to recreate the properties which were added previously to the Global Options object dynamically at runtime
LPARAMETERS oOptionsObj
LOCAL ARRAY laSection(1)
IF VARTYPE(oOptionsObj)<>"O"
RETURN .f.
ENDIF
*-- read whole section from storage file
this.oini.getinisection(@laSection, ;
this.csectionname, this.cinifile)
IF VARTYPE(laSection(1))="C"
*-- if something was read from INI
FOR lni = 1 TO ALEN(laSection)
 IF NOT PEMSTATUS(oOptionsObj, laSection(lni), 5)
  oOptionsObj.AddProperty(laSection(lni))
 ENDIF
NEXT
ENDIF

Let's start the engine

Now, we have all pieces of the puzzle required to build the Global Options object. We have the options class (bzOptions), derived from the bzAccessAssign class, which triggers PropertyAccess and PropertyAssign events when any public property is read or written. We have the bzOptionsStorage_INI class, which implements the generic storage interface defined by abstract class bzOptionsStorage_Base. All we have to do is connect the bzOptions class with the bzOptionsStorage_INI storage class. To do this, we create a new class bzOptions_INI, based on the bzOptions class, and set the properties __cStorageClass and __cStorageClassLib to the values bzOptionsStorage_INI and bz_optionsstorage.vcx.
 
Now, we're ready to create the Global Options object and play with it. To be able to test it from the FoxPro command prompt, open the project PropAccessAssign.VCX (from download file in the URL listed at the beginning of this article) and set its folder as the default directory in FoxPro.
 
Then, run the code to create the Options object, change its Comment property, and add two new properties dynamically (CurrentDate and CurrentTime).
After running the code, open Windows Explorer and browse to the project's home directory. You can do this from the FoxPro command prompt. Listing G shows the commands you type in the Foxpro command window.
 
Listing G: You create the Options object, change value of a property (Comment), and add two new properties dynamically at runtime (CurrentTime and CurrentDate).
*-- create the object, change Command property and add tow new properties
Set default to (_vfp.activeProject.HomeDir)�
oDemo = NEWOBJECT("bzoptions_ini", "bz_appoptions")
oDemo.Comment = "My test property"
oDemo.AddProperty("CurrentTime", datetime())
oDemo.AddProperty("CurrentDate", date())

*-- Open Windows Explorer to look for INI file
lcDefa = _vfp.ActiveProject.HomeDir
RUN explorer &lcDefa.

You'll see the file APPOPTIONS.INI, with the following content (with date and time read from your system):
 
[GLOBAL OPTIONS]
COMMENT=My test property
CURRENTTIME=7/02/2006 1:14:58 AM
CURRENTDATE=7/02/2006
 
You change the value of any property (for example, change value of the COMMENT setting) and save; then, type the following in the FoxPro command window:
 
?odemo.Comment

A demo program

To show how to use an options class in a real program, we created a small FoxPro application. It uses the TESTDATA database shipped with Visual FoxPro (located in the Samples\Data folder). For the purpose of this demo, we created a new library, DEMO_APPOPTIONS.VCX, which contains two classes: a Global Options class, named clsDemoOptions, inherited from the bzOptions_ini class, and clsDemoOptionsStorage, inherited from bzOptionsStorage_ini. clsDemoOptions is the Global Options class for the demo program. It contains four custom properties, which represents application options. clsDemoOptionsStorage is the storage class. It handles storage to a custom INI file, named DEMOOPT.INI.
 
Class clsDemoOptions has four properties used for store global options:
  • cCompanyName. Name of the company who uses the program.
  • cCompPhone. Phone number of the company.
  • nCustReportOption. Numeric; it allows users to see all customers on a report, or to filter them based on country.
  • cCountry. Stores the country for which report data was filtered.

The program opens the TESTDATA database, which instantiates the Global Options object and starts the main form, as shown in Figure C.

Figure C: The demo program's main form lets users change options and view the results in two different parts of the program.
 
ft206b1c.gif
 
The Options button opens the options form. It allows you to edit global options for the program. The Load event creates an empty object and stores its reference in property oOpt. This object caches all properties (options) of the program, as shown in Listing H.
 
Listing H: Code for Load event of the Options form
LOCAL lnMemb, i, lcProp
local ARRAY laMemb(1)
*-- a new object is creared based on type EMPTY
thisform.oopt = CREATEOBJECT("empty")
*-- all properties are read
lnMemb = AMEMBERS(laMemb, oGlobalOpt, 0, "G")
FOR i = 1 TO lnMemb
lcProp = laMemb(i)
*-- add the prop to cache object
ADDPROPERTY(thisform.oOpt, lcProp)
TRY�
  *-- assign the value
  *-- embed in try ... endtry to ignore properties
  *-- which cannot be assigned, like collections�
  *-- or objects
  thisform.oOpt.&lcProp. = oGlobalOpt.&lcProp.
CATCH
ENDTRY
NEXT�

 
When a user clicks the OK button, the values that were changed are saved back to the Global Options object. Notice that the values from the Options form are compared against current values from the Global Options class. This means they're actually compared against the latest updated values from storage (because, as you know, each time a value of the property is read, it's actually read from the storage file). The code that saves the options is shown in Listing I.
 
Listing I: The values of settings changed in Options form are saved back to Global Options object
LOCAL lnMemb, i, lcProp
local ARRAY laMemb(1)

*-- read all properties from cache object
lnMemb = AMEMBERS(laMemb, thisform.oOpt , 0, "G")
FOR i = 1 TO lnMemb
lcProp = laMemb(i)
*-- if value on screen is different than value�
*-- from global options object, update it
IF oGlobalOpt.&lcProp. <> thisform.oOpt.&lcProp.�
  oGlobalOpt.&lcProp. = thisform.oOpt.&lcProp.�
ENDIF�
NEXT�
RELEASE thisform
 
In the Edit Customers form, on the form header, there are two controls, which display the company name and the phone number. The corresponding values are read in the form's Init event from the Global Options class and assigned to corresponding controls.
 
The options class doesn't support controls on forms to be bound directly to its properties. Instead, you have to assign the values of the properties in code to the controls in forms. However, it's possible to bind controls from reports directly to properties of the Global Options class. In Figure D, you can see that the control showing the company name is bound directly to the cCompanyName property of the Global Options object.
 
Figure D: You can bind a control on from a report directly to a property of the Global Options object.
 
ft206b1d.gif
 
To test the demo program, compile it to an EXE file and start two instances. In one instance, you can change some properties (for example, the country, which selects the customers on report, or the company name). On another instance, try to run the report, and you'll see the company name changes automatically to the new value.
 
   
  
 Plugin Architecture    

Plugin architecture for ultimate flexibility

Nowadays, computer users are very demanding, and sometimes it isn't possible to imagine all their requirements when you design the application. One way to accommodate this need for flexibility is to have your application work with plugins. Plugins are small modules that can be compiled into their own executable file and deployed independently. The main program recognizes those plugins and loads them dynamically at runtime, giving it enhanced functionality.
 
However, some of the plugins may need to use their own settings. You can store these settings in a Global Options object, along with all other settings of the program. Unfortunately, when you create the Global Options object in the main program, you probably know little about what plugins you'll create over time and what settings they'll need. Fortunately, Visual FoxPro allows you to create properties for an object dynamically at runtime, using either the AddProperty method or the ADDPROPERTY() function. When you install the plugin, it can create the required properties in the Global Options object. Their values are then saved along with the initial properties in the storage. When someone restarts the program or starts it on another computer, RecreateOptionsObject detects those properties in the storage file and re-creates the appropriate properties in the Global Options object.