Customizing VFP's Tools





Session ???



Tamar E. Granor, Ph.D.

Voice: 215-635-1958

Email: tamar_granor@compuserve.com





Overview



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.

Tools and More Tools

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.

Replacing Xbase tools

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

Table 1 FoxPro's Xbase tools –Each Xbase tool has a system variable that points to the code to run.

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.

Open Architecture

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.

Table 2 Designer Open Architecture – Most of VFP's built-in tools store their data in a table with special extensions.

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.

Add-ins

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.

Adding to the Class Browser

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.

Figure 1 Using the Class Browser – The Class Browser lists classes and their members.

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.

Registering Class Browser add-ins

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


Structuring a Class Browser add-in

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.

Table 3 Class Browser PEMs – The Class Browser has a rich object model. These properties and methods are likely to be used in many add-ins. See Help for the complete list.

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.

Using a Class Browser add-in

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.

Figure 2 Running an add-in – To run an add-in that isn't attached to a Class Browser event, choose Add-ins from the context menu.

An example: Subclassing a class library

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.

Figure 3 The Create Sublibrary add-in – This add-in subclasses every class in the current class library. The user specifies the destination and deals with naming conventions.

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.

Figure 4 Handling duplication – If a class in the source class library already exists in the destination class library, the user can choose whether to overwrite it.

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.

Enhancing the Component Gallery

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.

Table 4 Component Gallery properties – Unfortunately, these aren't documented anywhere. This list shows some of the key items.

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.

Extending the Coverage Profiler

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.

Figure 5 Running a Coverage Profiler add-in – This dialog allows you to find, run, and register Coverage Profiler add-ins.

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.

Customizing the Task List

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.

Figure 6 The Task List – This tool, new in VFP 7, lets you manage reminders and pieces of code that need attention.

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.

An example: A Coverage Profiler add-in to add tasks to the Task List

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.

Figure 7 Creating tasks automatically – This Coverage Profiler add-in adds a task for each line in the log that meets the specified criteria.

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.

Building a Builder

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.

Registering builders

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.

Table 4 Registering a builder – To make your builder available through VFP's Builder mechanism, add a record to Builder.DBF containing these fields.

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.

Structuring a 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.)

Extending the builder system

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.

An example: Replacing base class objects

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:

bullet

About is a documentation method that describes the form.

bullet

EnableControls handles enabling and disabling of controls on the form as changes are made.

bullet

ReplaceControl performs the actual replacement of the selected control with the newly chosen control.

Figure 8 The Base Class Builder – This builder lets you replace a control with another control derived from the same base class.

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</