Customizing VFP's Tools
Session ???
Tamar E. Granor, Ph.D.
Voice: 215-635-1958
Email: tamar_granor@compuserve.com
Many of Visual FoxPro's tools, such as the Class Browser, the Builder system and the Coverage Profiler, are extensible. This session looks at the architecture of some of VFP's tools and demonstrates how to "have it your way." Familiarity with VFP and some experience using the tools is assumed.
Visual FoxPro includes a large collection of tools. Some, like the Form and Class Designers, are built right into the product. But many others are written in Visual FoxPro – Microsoft refers to these collectively as "Xbase tools" (because they're written in FoxPro, that is, Xbase, rather than C). The Xbase tools include the Class Browser and its alter ego, the Component Gallery; the Builders and Wizards; the Coverage Profiler; and, in VFP 7, the IntelliSense Manager; the Task List Manager; and the Object Browser.
VFP has a system variable corresponding to each of the Xbase tools. The variable points to the program to run for that tool. For example, the _Browser variable points to "Browser.APP" in the VFP home directory. Table 1 shows the list of Xbase tools, their corresponding system variables and the default values of those variables. (Note that there are some other system variables that point to programs, but those routines either don't fit the definition "tool," or aren't written in Xbase.)
|
Tool |
System Variable |
Default value |
|
Builders |
_BUILDER |
HOME() + "Builder.App" |
|
Class Browser |
_BROWSER |
HOME() + "Browser.App" |
|
Component Gallery |
_GALLERY |
HOME() + "Gallery.App" |
|
Coverage Profiler |
_COVERAGE |
HOME() + "Coverage.App" |
|
IntelliSense Manager |
_CODESENSE |
HOME() + "FoxCode.App" |
|
Object Browser |
_OBJECTBROWSER |
HOME() + "ObjectBrowser.App" |
|
Task List Manager |
_TASKLIST |
HOME() + "TaskList.App" |
|
Wizards |
_WIZARD |
HOME() + "Wizard.App" |
The system variables, in fact, provide an alternative way to run the various tools. Rather than choosing a tool from the menu or toolbar, you can also run it by DOing the corresponding variable. For example, to start the Coverage Profiler, you can issue this command:
DO (_COVERAGE)
One way to customize a tool is to replace it entirely and set the appropriate variable to point to your replacement. That's the strategy used by the GENMENUX menu generator wrapper program. To use it, you set _GENMENU to point to GenMenuX.PRG.
One of the built-in tools can also be replaced – the Expression Builder. Set _GETEXPR to point to the program you'd rather use and both the IDE and calls to GETEXPR use the program you specify.
In VFP 6 and later, the source code for the Xbase tools comes with VFP. (Unzip XSource.ZIP in the Tools directory.) So another customization strategy is to modify or subclass the existing code, if it doesn't do exactly what you want.
But the truth is that most of us would find either replacing or modifying code for any of the tools a daunting task.
Fortunately, there are other ways. Several of the tools (the Class Browser/Component Gallery, the Coverage Profiler, and the Object Browser) accept "add-ins," pieces of code you write and then can call from specified points in the tool.
The Builder and Wizard system is table-driven. To add a builder or wizard, you add a record to the appropriate driver table.
While the IntelliSense Manager itself can't be customized, IntelliSense in VFP is itself table-driven. (The table is referenced through the _FOXCODE system variable.) The IntelliSense Manager provides several ways of changing IntelliSense behavior.
The Task List Manager is also table-driven, with the table referenced through the _FOXTASK system variable. In addition, its interface provides the ability to add custom fields to tasks and have any or all of those fields displayed in the Task Manager. The Task List's object model also makes it possible to add and manipulate tasks programmatically.
Even VFP's built-in tools, like the Project Manager and most of the Designers, have open architecture. Each of them stores their data in a table that uses special extensions. Table 2 shows the file extensions used for each tool.
|
Tool |
Table (DBF) |
Memo fields (FPT) |
|
Class Designer |
VCX |
VCT |
|
Form Designer |
SCX |
SCT |
|
Label Designer |
LBX |
LBT |
|
Menu Designer |
MNX |
MNT |
|
Project Manager |
PJX |
PJT |
|
Report Designer |
FRX |
FRT |
It's possible to directly manipulate data from these tools by modifying the tables directly. In addition, projects offer the ProjectHook class, which has methods that fire when various events occur on the project. For example, you can write code to run whenever a file in the project is modified.
An add-in a program hooked into another program. It generally manipulates the properties of the host program, and may use the host's methods to do so. An add-in performs a task not built into the host program.
The Class Browser, Component Gallery, Coverage Profiler and Object Browser all support add-ins. However, the techniques for specifying and using them vary with the tool.
The Class Browser and Component Gallery (which are really two faces for the same tool) have an AddIn method that lets you register and unregister add-ins. Add-ins in the CB/CG can either be added to a menu of add-ins or hooked to a specific event, such as the Click of a particular button.
The Coverage Profiler has an Add-ins item on its context menu and toolbar. When you choose an add-in to run, you can also specify that it should be registered and thus made available for future Profiler sessions. In addition, you can use the AddTool method of the Coverage Profiler's main form to add a button or other control that runs an add-in.
The Object Browser also includes an Add-ins item on its context menu that offers the opportunity to install an add-in. Add-ins can also be installed and removed using the Add-ins page of the Options dialog.
Add-ins for the Class Browser, Component Gallery and Coverage Profiler are usually programs, but can be any directly executable piece of code (such as a form, an APP or an EXE). Object Browser add-ins are classes, usually VCX-based, and work best when subclassed from the _baseaddin class provided.
The Class Browser provides an easy way to look inside class libraries to see the relationships among classes, as well as the structure of individual classes. It's a remarkably capable tool. (For thorough coverage of the Class Browser's abilities, see the Egger book listed in the "Resources" section.) Figure 1 shows the Class Browser listing the classes for the SuperClass toolbar.

However, there are some tasks you might want to do with your class libraries that aren't included. To give you the ability to add such tasks, the Class Browser includes add-in capability.
Whenever an instance of the Class Browser is running, a public variable _oBrowser is created, if necessary, and given an object reference to the active Class Browser object. This reference is useful for registering add-ins, as well as for checking the state of the Class Browser.
Attaching an add-in to the Class Browser is easy. When you call the Class Browser's AddIn method, the add-in is added to the Class Browser's registration table (Browser.DBF in the VFP home directory). From that point on, the add-in is available. Add-ins do not need to be re-registered each time you run VFP or the Class Browser.
The AddIn method has six parameters, but you'll usually use only the first three. The first two are required. Here's the syntax:
_oBrowser.AddIn( cName , cProgram [, cMethod ])
cName is the name of the add-in, which appears on the add-in menu, if the cMethod parameter is omitted. cProgram is the program to run when the add-in is called. The optional cMethod parameter lets you specify that the add-in should run automatically when a particular Class Browser event fires.
For example, to register an add-in called MyAddIn, which is implemented by MyAddIn.PRG in the folder c:\apps\utils, you'd issue:
_oBrowser.AddIn( "MyAddIn", "c:\apps\utils\MyAddIn.PRG")
To register the same add-in, but set it up to fire whenever the Class Browser's Activate method fires, register it like this:
_oBrowser.AddIn( "MyAddIn", "c:\apps\utils\MyAddIn.PRG", "Activate")
To register an add-in to fire based on an event of one of the Class Browser's controls, include the control name in the event name. For example, this call indicates that the add-in should fire when the user right-clicks on the Add button:
_oBrowser.AddIn( "MyAddIn", "c:\apps\utils\MyAddIn.PRG", ;
"cmdAdd.RightClick")
A given add-in can only be fired in one way, so if we issued the three calls to AddIn shown here in sequence, the add-in would be called only when the user right-clicks on the Add button.
Finally, to unregister an add-in, call the AddIn method, but pass .null. for the cProgram parameter. For example, to unregister the add-in registered above, issue:
_oBrowser.AddIn( "MyAddIn", .null. )
When a Class Browser add-in is called, no matter what triggers it, it receives a single parameter: an object reference to the Class Browser itself. This provides the add-in with the ability to access the Browser object's properties and methods without using the _oBrowser variable. This is important because it's possible to have multiple instances of the Class Browser open, so you don't know which instance the _oBrowser variable points to.
The add-in itself can contain any code at all. For testing purposes, you may want to create a simple add-in, like the following:
LPARAMETERS oBrowser
WAIT WINDOW "This is the add-in"
RETURN
However, a useful add-in is likely to work with the Class Browser's object model. Its properties and methods are documented in the VFP Help file (see the topics "Class Browser Properties" and "Class Browser Methods"). Some of the key PEMs you're likely to work with are listed in Table 3.
|
Name |
Property/Method |
Purpose |
|
aClassList |
Property |
An array property containing a list of all the classes and forms in the class list, with one row for each. |
|
AddFile |
Method |
Opens an existing class library or form, without closing the files that are already open. |
|
aFiles |
Property |
An array property containing a list of all the files currently open in the Class Browser, with one row for each file. |
|
cClass |
Property |
Contains the name of the currently selected class in the class list. |
|
cClassLibrary |
Property |
Contains the name of the class library for the currently selected class in the class list. |
|
cFileName |
Property |
The full path to the file containing the item currently selected in the class list. |
|
ExportClass |
Method |
Generates and, optionally, displays code for the class, form or file selected in the class list. |
|
ModifyClass |
Method |
Opens the currently selected class in the Class Designer, optionally opening the method editor to a specified method. |
|
NewFile |
Method |
Creates a new class library and, optionally, opens it in the Class Browser. |
|
OpenFile |
Method |
Opens an existing class library or form in the Class Browser, closing the files that are already open. |
|
RedefineClass |
Method |
Changes the parent class of the currently selected class. |
|
RemoveClass |
Method |
Removes the selected class from its class library. |
|
RenameClass |
Method |
Renames the selected class in the class list. |
|
SeekClass |
Method |
Moves the class list pointer to the specified class. |
|
SeekMember |
Method |
Moves the member list pointer to the specified member. |
|
SeekParentClass |
Method |
Moves the class list pointer to the parent class of the currently selected class, opening the library containing the parent class, if necessary. |
Running a Class Browser add-in is much easier than writing it or registering it. If the add-in is hooked to a Class Browser event, simply triggering that event runs the add-in. If the add-in was placed on the Class Browser menu, right-click anywhere on the Browser, except in the areas that hold the class list and member list. The context menu that appears includes an Add-ins… item. (Figure 2 shows the Class Browser's context menu.) Choose that item and a list of menu add-ins appears. Choose the add-in you want to run.

Whenever I start a new project, I want to subclass each of my personal base classes, to provide a starting point for the project. This task sounds like something that should be easy in the Class Browser, but in fact, doing it there is quite a tedious, manual operation. Clearly, an automated process is called for.
The add-in should prompt for a name and location for the destination class library, then make a copy of every class from the currently selected class library (the source), placing the copies in the destination library. Before moving to code, we need to consider some other issues for the add-in.
Many people use prefixes or suffixes on class names to indicate their position in the class hierarchy (distinguishing abstract classes from concrete) or to specify the client or project a class is meant for. So, the add-in needs a way to change the prefix and/or suffix of the class name.
Figure 3 shows the main interface to the Create Sublibrary add-in. It allows the user to specify the destination library, to change prefixes and suffixes, and to indicate whether the new library should be opened in the Class Browser after the task is done.

It's possible that some of the classes to be copied already exist in the destination class library. (For example, perhaps we've added some new classes to the base class library and want to update the various copies.) The best solution is to give the user control over what happens in this case. Figure 4 shows the approach used.

The add-in consists of a main program that calls a form defined in code. While an SCX-based form can be used as an add-in, a coded form allows the add-in to be distributed as a single file. The main program does a lot of error checking. If all is well, it instantiates and shows the form, passing an object reference to the Class Browser.
The form contains a tabless pageframe with two pages (shown in Figures 3 and 4). It creates its controls from a number of other classes defined in the same file. The form uses two cursors created in the Load method. Classes contains the list of classes in the library to be copied (the source), while Preexists holds the list of classes in the destination library that duplicate classes to be copied. The key methods are ValidateCopy, DetectDups and CopyLibrary.
ValidateCopy drives the whole copy process-it's called by the OK button on the first page (Figure 3). First, it creates the new name for each class by removing the old prefix and suffix and adding the new ones. Then it updates the Classes cursor with the new name. When all the names have been transformed, DetectDups is called to create a list of duplicate class names. If any matches are found, the second page (Figure 4) is activated and the method ends. If no class names are duplicated, CopyLibrary is called to perform the copy. Here's the code for ValidateCopy:
PROCEDURE validatecopy
* Creates the new class names and posts them to the Classes Cursor
* Calls either the copy method or the page with the grid for resolving
* the duplicate names
LOCAL lcNewClassName, lcNewPreFix, lcOldPrefix, lcNewSuffix,
LOCAL lcOldSuffix
SELECT Classes
WITH Thisform.pageframe1.page1
SCAN
* Get the old class name in the existing library
lcNewClassName = ALLTRIM(OldClassName)
IF .chkRetainNames.Value
* If no mods were requested, post the old name as the new
* and get the next one
REPLACE NewClassName WITH lcNewClassName
LOOP
ENDIF
* Get the name change requests
lcNewPrefix = ALLTRIM(.txtNewPrefix.Value)
lcNewSuffix = ALLTRIM(.txtNewSuffix.Value)
lcOldPrefix = ALLTRIM(.txtOldPrefix.Value)
lcOldSuffix = ALLTRIM(.txtOldSuffix.Value)
IF NOT EMPTY( lcOldPreFix )
* If a prefix is to be removed
IF UPPER(LEFT(lcNewClassName,LEN(lcOldPrefix))) = ;
UPPER(lcOldPrefix)
* If that prefix is on this class name, remove it
lcNewClassName = SUBSTR(lcNewClassName,LEN(lcOldPrefix)+1)
ENDIF
ENDIF
IF NOT EMPTY( lcNewPreFix )
* If a prefix is to be added, add it
lcNewClassName = ALLTRIM(lcNewPrefix) + lcNewClassName
ENDIF
IF NOT EMPTY( lcOldSufFix )
* If a suffix is to be removed
IF UPPER(RIGHT(lcNewClassName,LEN(lcOldSuffix))) = ;
UPPER(lcOldSuffix)
* If that suffix is on this class name, remove it
lcNewClassName = SUBSTR(lcNewClassName,1,;
LEN(lcNewClassName)-LEN(lcOldSuffix))
ENDIF
ENDIF
IF NOT EMPTY( lcNewSufFix )
* If a suffix is to be added, add it
lcNewClassName = lcNewClassName + ALLTRIM(lcNewSuffix)
ENDIF
* Post the new name
REPLACE NewClassName WITH lcNewClassName
ENDSCAN
ENDWITH
* Detect any classes that may preexist in the destination library
ThisForm.DetectDups()
* Did any get found
LOCATE FOR Preexists
IF FOUND()
* If yes, display the grid for overwrites
ThisForm.Pageframe1.ActivePage = 2
* Since the grid page does the copying if requested get out of here
RETURN
ENDIF
* Do the actual copying of the classes
Thisform.CopyLibrary()
RETURN
ENDPROC
DetectDups checks the new names against the list of classes in the destination library and adds a record to Preexists for each match it finds. Here's the code:
PROCEDURE detectdups
* Searches target library for existing classes with duplicate names
LOCAL nExisting, aExistingClasses[1], nMatches, aMatches[1], nMatchPos,
LOCAL lcNewLib, lcOldLib
lcOldLib = ALLTRIM(Thisform.Pageframe1.Page1.txtSourceLibrary.Value)
lcNewLib = ALLTRIM( ;
Thisform.Pageframe1.Page1.txtDestinationLibrary.Value)
IF NOT FILE( lcNewLib )
* The target library does not exist, then there is nothing to do here
RETURN
ENDIF
SELECT Classes
* Get a list of classes in the target library
nExisting = AVCXClasses( aExistingClasses, lcNewLib )
IF nExisting > 0
* If there are any, compare existing classes to classes in
* source library
nMatches = 0
FOR nClass = 1 TO nExisting
* Search the source new class names for a match
LOCATE FOR UPPER(TRIM(NewClassName)) = ;
UPPER(TRIM(aExistingClasses[ nClass, 1 ]))
IF FOUND()
* If we found a matching name, mark the Preexists Field of the
* Classes cursor
REPLACE PreExists WITH .T.
INSERT INTO Preexists (NewClassName) ;
VALUES (Classes.NewClassName)
ENDIF
ENDFOR
ENDIF
RETURN
ENDPROC
CopyLibrary is called, as explained above, from the ValidateCopy method. It's also called from the Copy button on the second page of the form. The method creates the destination class library, if it doesn't already exist, then subclasses all the classes, except those marked by the user not to be overwritten.
While the Browser has a method called NewClass, unfortunately, it doesn't let you store the new class in a library other than the one currently selected. It also can only create classes based on the VFP base classes and the classes in the current library. So, creating the subclasses isn't as simple as calling NewClass for each class in the class library.
CopyLibrary uses the CREATE CLASS command instead, as it has all the flexibility needed. However, it doesn't have a NOSHOW clause to create the new class silently. CREATE CLASS always opens the Class Designer, so CopyLibrary uses KEYBOARD to issue the appropriate keystrokes to save the new class and close the designer.
Here's the code for CopyLibrary:
PROCEDURE copylibrary
LOCAL lcNewLibrary, lcOldLibrary, lcNewClass, lcOldClass
* Get name of the target library
lcNewLibrary = ALLTRIM( ;
Thisform.PageFrame1.Page1.txtDestinationLibrary.Value)
* Get name of the source library
lcOldLibrary = ALLTRIM( ;
Thisform.PageFrame1.Page1.txtSourceLibrary.Value)
* Check for any preexisting classes
SELECT Preexists
IF RECCOUNT() > 0
* If any preexisting classes mark them according to the users choice
SCAN FOR CopyOver
SELECT Classes
LOCATE FOR NewClassName = Preexists.NewClassName
REPLACE CopyOver WITH .T.
ENDSCAN
ENDIF
SELECT Classes
IF NOT FILE(lcNewLibrary)
* If the target library does not exist create it
CREATE CLASSLIB (lcNewLibrary)
ENDIF
* Process the classes in the source library
SCAN
IF NOT Classes.Preexists OR (Classes.Preexists AND Classes.CopyOver)
* If the class is new to the target or it is marked to be
* an over write
IF Classes.Preexists
* If this is an over write remove the existing class
* from the target
REMOVE CLASS (ALLTRIM(Classes.NewClassName)) OF (lcNewLibrary)
ENDIF
* Set up to shut down the Class Designer
KEYBOARD "{CTRL+F4}Y"
* Create the class
CREATE CLASS (ALLTRIM(Classes.NewClassName)) OF (lcNewLibrary) ;
AS (ALLTRIM(Classes.OldClassName)) FROM (lcOldLibrary)
ENDIF
ENDSCAN
* Now open this library if requested
IF THISFORM.Pageframe1.Page1.chkOpenLib.Value
This.oBrowser.AddFile(lower(lcNewLibrary))
ENDIF
RETURN
ENDPROC
The complete add-in is included in the materials for this session as NewLib.PRG. NLReadme.TXT is a readme file for this add-in.
The Component Gallery is another face for the same application as the Class Browser. (The main program of Gallery.APP calls Browser.APP, passing a parameter to indicate that it should open in "gallery mode.")
This means that the technique for registering add-ins is the same – call the AddIn method – and that Component Gallery add-ins also receive a reference to the calling Browser/Gallery instance.
Unfortunately, the resemblance ends there. While the Class Browser PEM's are well-documented and several articles have been written about extending the Class Browser, the Component Gallery's PEM's are totally undocumented (there aren't even descriptions of them in the main Browser form) and no such articles exist.
The best way to figure out what Component Gallery PEM's are relevant to a particular task is to open the Component Gallery, then explore its members in the Debugger and Command Window, using the _oBrowser reference. Table 4 shows some of the key properties for the Component Gallery.
|
Name |
Purpose |
|
aFolderList |
An array property containing one member for each catalog or folder available. Each item is an object reference to a _folder object. |
|
aItemList |
An array property containing one member for each item of each folder that's been examined during this session. Each array element is either an object reference to an object based on (or subclassed from) the _folder or _item class, or contains a delimited list with key information about the item. |
|
cCatalog |
The currently chosen catalog. |
|
nFolderCount |
The number of folders in aFolderList. |
|
nFolderListIndex |
The position of the currently selected catalog in aFolderList. |
|
nItemCount |
The number of items in aItemList. |
|
nItemListIndex |
The position of the currently selected item within the current folder. |
|
oCatalog |
An object reference to the currently selected catalog. |
|
oFolder |
An object reference to the currently selected folder. |
|
oItem |
An object reference to the currently selected item. |
The Component Gallery also offers an entirely different approach to extension. You can create your own item types that can be used in the Gallery just like the types provided. The Egger book listed in the Resources section explains this approach in detail.
The Coverage Profiler takes a coverage log (created by issuing SET COVERAGE TO <filename> or by turning coverage logging on in the Debugger) and turns it into meaningful information. It offers information about which lines were executed and which were not, as well as timing information for those lines which were executed.
The Coverage Profiler can be enhanced in a variety of ways. Its design separates interface from implementation - there's a coverage engine class and a coverage interface class. Each can be subclassed independently and it's also possible to create an entirely new user interface for the coverage engine. The Nicholls article listed in the Resources section discusses a variety of subclassing options.
The COV_TUNE.H include file (in the VFPSource\Coverage directory, when you unzip the provided source) allows you to fine-tune various Coverage Profiler behavior. It's well commented to show you what effects your changes will have. Of course, once you make changes, you'll need to rebuild COVERAGE.APP.
The simplest way to extend the Coverage Profiler, though, is with add-ins. As with the Class Browser/Component Gallery, it's easy to attach add-ins to the Coverage Profiler. The Coverage Profiler's toolbar and context menu both contain an Add-ins item. When you choose it, a dialog (shown in Figure 5) appears. The dialog allows you to locate and execute an add-in. The dropdown contains a list of all add-ins executed in this Coverage Profiler session.

The checkbox in the dialog lets you register an add-in. Once registered, the add-in appears in the dropdown every time you use the Coverage Profiler. By default, the list of registered add-ins is stored in the Registry (in the key HKEY_CURRENT_USER\Software\Microsoft\Visual FoxPro\<version>\Coverage). While there's no mechanism provided for unregistering an add-in, it is possible to delete the relevant Registry key.
Add-ins receive as a parameter an object reference to the Coverage Profiler engine object. There's also a public variable, _oCoverage, that points to this object.
The object model for the Coverage Profiler's engine is well documented in the VFP Help. (See the Coverage Engine Object topic.) Two properties you're likely to use in almost any add-in are cSourceAlias, which contains the alias for a cursor containing the parsed coverage log, and cTargetAlias, which contains the alias for a cursor containing processed coverage data.
Many Coverage Profiler add-ins won't need to deal with any other properties, but will simply process the data in one or both cursors. An example of a Coverage Profiler add-in that processes the raw log data follows the discussion of the Task List.
The Coverage Profiler makes it easy to add to its own interface. The cov_maindialog_standard form class, which is used for the Coverage Profiler's main form, has an AddTool method. This method accepts a class name as parameter and adds an instance of that class to the Coverage Profiler form.
VFP 7 offers a new tool, the Task List, which contains shortcuts to lines of code and any other tasks a developer chooses to add. Tasks can be added through the Task List interface (shown in Figure 6) or by setting shortcuts in code editing windows.

As with the other Xbase tools, the Task List's source code is provided. VFP 7 also includes two documents that describe the Task List's object model and specifications. However, there's no add-in mechanism and the object model is not documented in the Help file.
So why include this tool in a session on extending VFP's tools? Because, despite these shortcomings, there are a few things you can do with the Task List without subclassing or replacing its code.
As with the other tools, a public variable (_oTaskList) containing object reference to the tool is created when you open it.
The tool provides for a list of custom fields, in addition to the built-in list. Right-click and choose Options. There you can specify or create a "user-defined column table." This table must include a 10-character UniqueID field (used to link the custom fields to the standard ones), but otherwise you can specify whatever fields you want.
Since the task data is stored in a table, it's possible to add tasks programmatically. However, it's better to work through the Task List's object model than just use straight VFP code.
The technique for adding a new task is a little roundabout (but similar to the way you add items to Outlook). First, you request an empty task object using the GetTaskObject method. When you've filled it in, you add it to the list using the AddTask method. The task object has a property for each field, whether built-in or custom. The property name is the field name preceded by an underscore ("_"). For example, the Contents field is represented by an _Contents property.
Editing tasks uses a similar paradigm. The GetTask method accepts the unique id of a task and returns a task object. The UpdateTask method accepts a task object and updates that task in the table.
Generally, once you've run a coverage log and looked at the results, you have a number of items for your "to do" list. So, an obvious enhancement to the Coverage Profiler is an automated way of adding tasks based on coverage information.
This add-in decides what tasks to create based on the amount of time a line took to execute and the number of times it was executed. A form (see Figure 7) lets you choose the criteria for sending lines to the task list. Behind the scenes, the Coverage Profiler and Task List objects are used to figure out which lines are affected and to create a task for each.

This add-in is implemented as a single form (CovToTask.SCX in the session materials), with a number of custom properties and methods. The main processing method is ProcessLog, partially shown here. (Two additional IF blocks handle the other two conditions for adding tasks.)
LOCAL lRetVal
This.computelinetimes()
IF This.laddforavgtime
SELECT * ;
FROM LineTotal ;
WHERE nAvgTime > This.navgtime ;
INTO CURSOR AboveAverage
lSuccess = .T.
SCAN WHILE lSuccess
* Bail out if the addition fails
lSuccess = This.addtask( ObjClass, Executing, ProcLine, ;
HostFile, nAvgTime, "Average Time")
ENDSCAN
USE IN AboveAverage
lRetVal = lSuccess
ENDIF
* Code to process the log for the other two conditions
* omitted for space
The ComputeLineTimes method contains a single query that consolidates information from the coverage log. Note the use of the cSourceAlias property of the Coverage object:
* Total time, number of times, average time,
* and maximum time for each line
SELECT objclass, executing, procline, hostfile,;
sum(duration) AS nTotalTime, count(*) AS nTimes, ;
avg(duration) AS nAvgTime, MAX(duration) AS nMaxTime ;
FROM (This.ocoverage.cSourceAlias) ;
GROUP BY objclass, executing, procline, hostfile ;
INTO CURSOR linetotal
The AddTask method ensures that we have a reference to the Task List, then creates and adds a task. (Assertions for parameter checking are omitted here.)
* Add a task to the task list
LPARAMETERS cObjClass, cExecuting, nProcline, cHostfile, nTime, cReason
* First four params come right from coverage log
* Fifth parameter is measured time
* Last parameter is test failed
* Assertions omitted for space
LOCAL oTask
IF This.Gettasklist()
WITH This.oTasklist
oTask = .GetTaskObject()
WITH oTask
._Type = "S"
IF UPPER(JUSTEXT(cHostFile)) <> "SCX"
._Class = ALLTRIM(cObjClass)
ELSE
._Class = ""
ENDIF
IF INLIST(UPPER(JUSTEXT(cHostFile)), "SCX", "VCX")
._Method = ALLTRIM(cExecuting)
ELSE
._Method = ""
ENDIF
._Line = nProcLine
._FileName = ALLTRIM(cHostFile)
._Contents = This.FindCodeLine( cHostFile, cExecuting, ;
nProcLine )
._cReason = ALLTRIM(cReason) + " " + TRANSFORM( nTime )
ENDWITH
.AddTask( oTask )
ENDWITH
lRetVal = .T.
ELSE
lRetVal = .F.
ENDIF
RETURN lRetVal
AddTask uses two additional methods of the class. GetTaskList gets a reference to the task list object, starting the tool, if necessary. It also ensures that the user-defined column table exists and includes a column called cReason. (To do so, it uses an instance of another class, cusSaveTable, that includes methods to store information about all open instances of a table, and to reopen a table in all data sessions where it was previously open.) FindCodeLine returns the actual code for a specified line-it's fairly standard VFP string-handling code.
This add-in reveals a bug in VFP's internal handling of task shortcuts. If the case in the Method field doesn't exactly match VFP's "native" case, the shortcut icon doesn't appear when the file is opened. ("Native" case means camel-case for method names and defined case for controls.) However, the file can still be opened to the right method with the cursor on the right line. Microsoft is aware of the bug.
The Builder technology introduced in VFP 3 may be one of the most underused facilities in the product. Because almost all the builders included with VFP are just wizard-like formatting and data set-up tools, very few developers realize the potential of builders. In fact, builders let you modify forms, controls and classes under construction. Builders don't have to have a user interface, but can perform all their actions behind the scenes.
A builder can be as simple as a few lines that grabs an object reference to something from a designer and changes a few properties, or it can be far more complex.
Builders can be run in a variety of ways, as well, and do not have to be hooked into the built-in Builder mechanism. You can run a builder from the Command Window or a menu item, if you choose. However, the built-in mechanism makes it easy to make a builder available whenever it's appropriate.
All builders registered in the Builder.DBF table located in the Wizards subdirectory of VFP are available through the built-in mechanism. The structure of the Builder table is shown in Table 4. (It's also documented in the VFP Help file.) You don't generally need to specify the ClassLib, ClassName and Parms fields. It's sufficient to provide Name, Descript, Type and Program.
|
Field name |
Type |
Content |
|
Name |
Character |
The name for your builder. This name is displayed in the Builder Selection dialog, if there's more than one appropriate builder available. |
|
Descript |
Memo |
The description of your builder. Displayed in the Description editbox of the Builder Selection dialog. |
|
Bitmap |
Memo |
Currently unused |
|
Type |
Character |
The control class to which your builder applies. Specify "ALL" for a builder that can be used on any type of control. Specify "MULTISELECT" for a builder that can be applied to multiple controls simultaneously. |
|
Program |
Memo |
The name, including path, of the program or application to run for this builder. |
|
ClassLib |
Memo |
The name, including path, of the class library containing the builder. |
|
Class |
Memo |
The name of the class within ClassLib that constitutes the builder. |
|
Parms |
Memo |
Parameters to pass to the builder. |
You can create the necessary record manually, by opening the table and using INSERT INTO or BROWSE. However, a really handy strategy is to create a main program for your builder that registers it, if necessary, then runs the actual builder. (Thanks to Doug Hennig for this idea.) Here's an example (drawn from the builder discussed later in this section and included in the session materials):
*=======================================================================
* Program: BASEBUILDERMAIN.PRG
* Purpose: Run the base class builder. Self-register, if
* necessary.
* Author: Tamar E. Granor
* Copyright: (c) 2001, Tamar E. Granor
* Last revision: 07/05/01
* Parameters: As passed by the builder system
* Returns: (None)
* Environment in:
* Environment out:
*=======================================================================
* Main program for BaseBuilder
LPARAMETERS uP1, uP2, uP3, uP4, uP5, uP6, ;
uP7, uP8, uP9, uP10, uP11, uP12
* Accept parameters passed by the builder system
#DEFINE ccMAIN "BASEBUILDERMAIN"
LOCAL nOldSelect
* Self-register if called directly
IF PROGRAM(0) == ccMAIN
nOldSelect = SELECT()
SELECT 0
USE HOME() + "Wizards\Builder" AGAIN
LOCATE FOR UPPER(Name) = "BASE CLASS BUILDER"
IF NOT FOUND()
m.Name = "Base Class Builder"
m.Descript = "Choose a class for base class controls"
m.Type = "ALL"
m.Program = SYS(16)
INSERT INTO Builder FROM MEMVAR
ENDIF
USE IN Builder
SELECT (nOldSelect)
ENDIF
* Run the actual builder
DO FORM ADDBS(JUSTPATH(SYS(16))) + "BaseBuilder" WITH ;
uP1, uP2, uP3, uP4, uP5, uP6, ;
uP7, uP8, uP9, uP10, uP11, uP12
RETURN
The program first checks to see whether it was run directly (as opposed to through the Builder mechanism). If so, it checks the Builder table to see whether this builder is already registered. If not, it registers it by inserting a record into table. After all that, it runs the program (a form, in this case) that constitutes the actual builder.
The builder application passes up to 12 parameters to builder programs, so the main program or Init method of any registered builder must accept those parameters. Having done so, however, it's unlikely that you need to do anything with the parameters.
Most builders are meant to work on the currently selected object or objects. The ASelObj() function creates an array of object references to those objects, one per row. (The function is also capable of retrieving a reference to the container object of the selected objects, or to the data environment of the containing form.)
Given a reference to the selected object or objects, you can write code that reads or modifies properties and methods. Since builders operate at design-time, it's even possible to insert method code using the WriteMethod method. Using AddObject and RemoveObject, you can change the contents of the form or class. In VFP 6 and later, you can add properties to the objects using the AddProperty method.
Like the builders provided with VFP, your builders can display an interface that gives the user options. However, for some builder tasks, no interface may be needed. For example, you might write a builder that replaces all base class controls with your subclasses. (The example below is a variation on this theme.)
VFP's builder mechanism has a hidden feature that can be especially useful in team development situations where one developer creates classes that other developers use. If a class has a Builder property, requesting a builder for that class runs the program specified in the Builder property. So the control developer can create both the control classes and builders that set up the controls properly. Other developers can drop controls on forms (or other classes), then right-click and choose Builder and be prompted to fill in the information necessary for the control to operate.
Ken Levy, tool developer extraordinaire, has extended the builder mechanism to make creating builders easier. The BuilderB and BuilderD technologies make it possible to create builders without starting from scratch each time. These approaches are well-documented in the Hennig white paper and FoxTalk articles listed in the Resources section.
When dropping controls onto forms (or onto classes), it's easy to use a control from the wrong class. It's especially common to use a base class control instead of the one you really want. Once you've set properties and added code, changing the control is a pain. This builder (BaseBuilder.SCX in the session materials) solves that problem by letting you replace a control with any control derived from the same base class.
The builder form (shown in figure 8) has one custom property, oControl, which holds an object reference to the control being replaced. It also has three custom methods:
About is a documentation method that describes the form. EnableControls handles enabling and disabling of controls on the form as changes are made. ReplaceControl performs the actual replacement of the selected control with the newly chosen control. |

Much of the work of the form is done in a custom control, cntCGClass (found in Utils.VCX in the session materials), which allows the user to choose a class from those in Component Gallery catalogs. This control uses three combos to list the catalogs, the folders within the currently chosen catalog and the controls of the right base class within the currently chosen folder. The control works directly with the Component Gallery's data tables.
Here's the code for the ReplaceControl method of the form:
* Replace the specified control with
* a control of the chosen class.
LOCAL oForm, oOriginalControl
LOCAL cClass, cClassLib, aOldProps[1]
cClass = This.cntcgclass.getchosenclass()
cClassLib = This.cntcgclass.GetChosenClasslib()
* First, get a reference to the containing form
oForm = This.oControl
DO WHILE UPPER(oForm.Baseclass) <> "FORM"
oForm = oForm.Parent
ENDDO
* Save a reference to the original control
oOriginalControl = This.oControl
* Add the new control
cTempName = SYS(2015)
oForm.NewObject( cTempName, ;
cClass, cClassLib )
oControl = EVALUATE( "oForm." + cTempName)
* Copy properties
* Get properties for the original object
* Note work-around in the next line, using "#+" for flags, when
* only "#" is needed.
nOldMembers = AMEMBERS(aOriginalProps, oOriginalControl, 3,"#+")
FOR nMember = 1 TO nOldMembers
DO CASE
CASE "R" $ UPPER(aOriginalProps[ nMember, 5])
* Read-only, so skip it
CASE "NAME"=UPPER(aOriginalProps[ nMember, 1])
* Name property, we'll do it later
CASE "PROPERTY" $ UPPER(aOriginalProps[ nMember, 2])
* See whether the property has changed
* and the new control has this property.
* If so, copy it.
IF "C" $ UPPER(aOriginalProps[ nMember, 5]) ;
AND PEMSTATUS( oControl, aOriginalProps[ nMember, 1], 5)
cPropName = aOriginalProps[ nMember, 1]
IF EMPTY(oOriginalControl.ReadExpression(cPropName))
oControl.&cPropName = oOriginalControl.&cPropName
ELSE
oControl.WriteExpression(cPropName, ;
oOriginalControl.ReadExpression(cPropName))
ENDIF
ENDIF
CASE INLIST(UPPER(aOriginalProps[ nMember, 2]), "METHOD", "EVENT")
* See whether the method has changed
* and whether the new control has this method
* If so, copy the contents
IF "C" $ UPPER(aOriginalProps[ nMember, 5]) ;
AND PEMSTATUS( oControl, aOriginalProps[ nMember, 1], 5)
cMethod = aOriginalProps[ nMember, 1]
cMethodCode = oOriginalControl.ReadMethod(cMethod)
oControl.WriteMethod( cMethod, cMethodCode )
ENDIF
OTHERWISE
* We should never get here