04.08.2013 Views

Un-Mapping Mapped Network Drives Andrew Coates - dFPUG-Portal

Un-Mapping Mapped Network Drives Andrew Coates - dFPUG-Portal

Un-Mapping Mapped Network Drives Andrew Coates - dFPUG-Portal

SHOW MORE
SHOW LESS

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

FoxTalk<br />

Solutions for Microsoft® FoxPro® and Visual FoxPro® Developers<br />

<strong>Un</strong>-<strong>Mapping</strong> <strong>Mapped</strong><br />

<strong>Network</strong> <strong>Drives</strong><br />

<strong>Andrew</strong> <strong>Coates</strong><br />

6.0<br />

One commonly stored piece of information is the location of a file or folder. If that<br />

file or folder is on the network, however, different users might refer to the location<br />

through different drive mappings. In this article <strong>Andrew</strong> shows how to decode the<br />

mapped location using the Windows API so that any user can view or retrieve the<br />

file or folder.<br />

I<br />

learned something this week that I guess I should have known. The<br />

Win32API isn’t consistent across operating systems. There are functions<br />

that work fine under Windows NT but fail miserably when called under<br />

Win95. I found one when working on the subject matter for this article.<br />

<strong>Mapping</strong> network drives is a common way of simplifying network storage<br />

systems from a user’s point of view. <strong>Un</strong>fortunately for the cause of universal<br />

access, however, different users map network shares to different driver letters.<br />

If your application allows users to store links to files, how can you tell whether<br />

E:\COMMON DOCUMENTS\BUDGET98.XLS and W:\BUDGET98.XLS refer<br />

to the same document? Even more importantly, how can a third user who has<br />

neither E: or W: mapped retrieve the document?<br />

The answer is UNC. The <strong>Un</strong>iversal Naming Convention uses the<br />

format \\SERVER\SHARENAME\ to refer to the location of a file or folder.<br />

In the preceding example, the first user might have mapped drive E to<br />

\\SERVER_PDC\PUB, and the second might have mapped drive W to<br />

\\SERVER_PDC\COMMONDOC.<br />

Any Visual FoxPro function that accepts a path as a parameter will handle<br />

UNC paths. However, returning a UNC path from the getfile() and getdir()<br />

functions is a completely different matter. These functions are central to<br />

allowing your users to specify the location of files and folders. The getfile()<br />

function will return a UNC path, but only if the user navigates to the file<br />

Continues on page 3<br />

December 1998<br />

Volume 10, Number 12<br />

1 <strong>Un</strong>-<strong>Mapping</strong> <strong>Mapped</strong><br />

<strong>Network</strong> <strong>Drives</strong><br />

<strong>Andrew</strong> <strong>Coates</strong><br />

3 Editorial: Bug Fixes Available<br />

at microsoft.com<br />

Whil Hentzen<br />

6 Best Practices: Seeing<br />

Patterns: The Mediator<br />

Jefferey A. Donnici<br />

10 Reusable Tools: Mining<br />

for Gold in the FFC<br />

Doug Hennig<br />

15 ActiveX and Automation<br />

Review: Integrating ADO<br />

and Visual Basic into Your<br />

VFP Applications, Part 2<br />

John V. Petersen<br />

19 The Kit Box: Second Star<br />

on the Right and Straight<br />

on ’til Morning<br />

Paul Maskens and Andy Kramek<br />

23 December<br />

Subscriber Downloads<br />

EA Creating a Set of Business<br />

Rules and the Making of<br />

a BusinessRule Server<br />

Stephen Settimi<br />

EA To Open the Tables<br />

or Not to Open the Tables—<br />

That is the Question<br />

Jim Booth<br />

EA Follow-up: To PrintScreen<br />

or Not to PrintScreen<br />

Art Bergquist<br />

6.0<br />

Applies to VFP<br />

v6.0 (Tahoe)<br />

Applies to<br />

VFP v5.0<br />

Applies to<br />

VFP v3.0<br />

Accompanying files available online<br />

at http://www.pinpub.com/foxtalk<br />

Applies specifically to one of these platforms.<br />

Applies to<br />

FoxPro v2.x


2 FoxTalk December 1998<br />

http://www.pinpub.com


Bug Fixes Available<br />

at microsoft.com<br />

Whil Hentzen<br />

IN the olden days of Fox Software, the gang would fix<br />

bugs and release patch disks. Some of you remember<br />

the 11 sets of patches for FoxPro 2.0, including two that<br />

were released, ahem, four days apart. The patches were<br />

shipped out for free (I think), but you had to know about<br />

the patches and request them.<br />

Distribution of bug fixes is a lot easier these days—<br />

nearly painless—because of the Web: Just go to the site<br />

and download them. But you still have to know about<br />

them. And if you don’t “know someone,” how are you<br />

going to find out? You could visit the Web site of the<br />

manufacturer of every product you use, but that’s not<br />

always a great solution. And even if you had the time,<br />

sometimes it takes a lot of digging.<br />

Visual Studio SP-1 (”SP” = Service Pack—softie speak<br />

for bug fixes) is now available for free order and<br />

download from Microsoft. It addresses specific binary<br />

compatibility issues with certain runtime files in Visual<br />

Studio 6.0—none of which I found applicable to VFP—but<br />

it’s probably still worthwhile getting.<br />

A more important update—and one that’s fairly<br />

well hidden—is the new Setup Wizard. If you’ve gotten<br />

to the point of shipping a VFP 6 application, you’ve<br />

possibly run into one or more of the half-dozen defects in<br />

the current Setup Wizard. A couple were pretty ugly, I<br />

think, but remember that FoxPro 2.5 shipped, and then<br />

<strong>Un</strong>-<strong>Mapping</strong> . . .<br />

Continued from page 1<br />

through the <strong>Network</strong> Neighborhood—something a user<br />

who’s used to having a network drive mapped is unlikely<br />

to do. The getdir() function is even worse. There’s no way<br />

of getting it to return a UNC path to a folder (except in<br />

one specific instance—see the sidebar “Returning a UNC<br />

Path from getdir()” for details). Not only do the functions<br />

not return UNC paths, but VFP doesn’t provide any way<br />

to convert a mapped path to a UNC path.<br />

Win32API to the rescue!<br />

The Windows 32-bit API provides a function that<br />

accepts a mapped path and returns the UNC path that<br />

From the Editor FoxTalk<br />

everyone found out that the Standard version<br />

didn’t work—period. Turns out everyone was<br />

testing the Extended version with their brand-new<br />

386 machines. Similarly, testing the Setup Wizard is<br />

pretty tough, considering you need a functioning<br />

app first.<br />

What’s been fixed? Options for creating a DEP file<br />

are now saved in WZSETUP.INI. Set Mark To “.”<br />

doesn’t blow out the distribution disks; neither does<br />

installing multiple OCX files in the source directory.<br />

FOXHHELP.EXE is now copied correctly if you check the<br />

HTML Help Engine check box. And if you have two files<br />

with the same name but in different directories—like if<br />

you were shipping a couple of sets of tutorial data—each<br />

is copied correctly now.<br />

There’s also an updated VFP 6.0 Component Gallery,<br />

updates to several FoxPro Foundation Class (FFC) files,<br />

and an ActiveX control updater that will automatically<br />

update the Treeview, Listview, and Imagelist controls to<br />

the latest versions.<br />

Where do you get these new updates? Pop over<br />

to msdn.microsoft.com/vfoxpro, and select Samples<br />

and Downloads, Product Updates, or, if you want the<br />

direct URL, try http://msdn.microsoft.com/vfoxpro/<br />

downloads/updates.asp. Note that your copy must be<br />

registered before you can get access to the page. ▲<br />

corresponds to that mapped path. I searched through<br />

the API documentation and found a function called<br />

WNetGet<strong>Un</strong>iversalName() in MPR.DLL in the system<br />

directory. The function will accept the path to either a file<br />

or a folder with or without a trailing backslash.<br />

<strong>Un</strong>fortunately, this function is only available<br />

under Windows NT. I found this out the hard way,<br />

having developed and tested a routine using<br />

WNetGet<strong>Un</strong>iversalName() on my WinNT development<br />

box, I proudly installed it on a client’s Win95 machine,<br />

only to have it not work . Microsoft has a<br />

Knowledge Base article confirming that it doesn’t work<br />

under Win95 (there’s no mention in the article of Win98).<br />

So much for a single, consistent Win32API!<br />

http://www.pinpub.com FoxTalk December 1998<br />

3


What the article does mention, though, is a couple of<br />

workarounds. The first involves about 30 lines of C code<br />

that calls another couple of functions in MPR.DLL. The<br />

second mentions in passing that there’s yet another<br />

function in MPR.DLL called WNetGetConnection().<br />

WNetGetConnection() is not quite as versatile as<br />

WNetGet<strong>Un</strong>iversalName(). It only accepts a drive letter<br />

and a colon (for instance, H:) rather than a full path.<br />

With a little string manipulation, however, it’s pretty<br />

easy to split up a mapped path, pass the function what it<br />

wants, and then re-assemble a complete UNC path from<br />

the pieces.<br />

To take the drudgery out of calling the Windows API,<br />

I wrote a wrapper function called GetUNCPath() that I<br />

include in any project in which I call either getdir() or<br />

getfile(). You could, of course, write wrappers for getfile()<br />

and getdir() that called GetUNCPath().<br />

For example, if the pub share on the server_pdc<br />

server were mapped as drive s:<br />

? GetUNCPath('s:\documents')<br />

\\server_pdc\pub\documents<br />

? GetUNCPath('c:\temp')<br />

c:\temp<br />

? GetUNCPath('s:\documents\myfile.doc')<br />

\\server_pdc\pub\documents\myfile.doc<br />

The wrapper function itself is pretty straightforward.<br />

The complete function is shown in Listing 1 and is<br />

available in this month’s Subscriber Downloads at<br />

www.pinpub.com/foxtalk. It accepts the mapped drive<br />

path as a compulsory parameter and optionally a length<br />

for the UNC version of the path. This second parameter is<br />

most often passed by the function itself in a recursive call<br />

if the default buffer size guess isn’t large enough (more on<br />

this later).<br />

Listing 1. The complete GetUNCPath source code.<br />

* Program....: GetUNCPath.prg<br />

* Version....: 1.0<br />

* Author.....: <strong>Andrew</strong> <strong>Coates</strong><br />

* Date.......: September 28, 1998<br />

* Notice.....: Copyright © 1998 Civil Solutions, All<br />

* Rights Reserved.<br />

* Compiler...: Visual FoxPro 05.00.00.0415 for Windows<br />

* Abstract...: Wrapper to the API call that converts a<br />

* mapped drive path to the UNC path<br />

* Changes....:<br />

* Originally used WNetGet<strong>Un</strong>iversalName, but that<br />

* doesn't work under Win95 (see KB Q131416). Now uses<br />

* WNetGetConnection, which uses a string rather than a<br />

* structure so STRUCTURE_HEADER is now 0.<br />

lParameters tc<strong>Mapped</strong>Path, tnBufferSize<br />

* from winnetwk.h<br />

#define UNIVERSAL_NAME_INFO_LEVEL 0x00000001<br />

#define REMOTE_NAME_INFO_LEVEL 0x00000002<br />

* from winerror.h<br />

#define NO_ERROR 0<br />

#define ERROR_BAD_DEVICE 1200<br />

#define ERROR_CONNECTION_UNAVAIL 1201<br />

#define ERROR_EXTENDED_ERROR 1208<br />

#define ERROR_MORE_DATA 234<br />

#define ERROR_NOT_SUPPORTED 50<br />

#define ERROR_NO_NET_OR_BAD_PATH 1203<br />

Returning a UNC Path<br />

from getdir()<br />

If you pass getdir() an initial folder in UNC format, then that<br />

folder will be the initially selected folder, but it won’t appear<br />

in the list of drives in the dialog box (see Figure 1). If that<br />

folder or one of its subfolders is selected, then the string<br />

returned will be in UNC. Be careful, though. If you navigate to<br />

a mapped drive from the drive’s drop-down box, there’s no<br />

way to get back to the UNC drive.<br />

#define ERROR_NO_NETWORK 1222<br />

#define ERROR_NOT_CONNECTED 2250<br />

* Local decision - paths aren't likely to be longer<br />

* than this - if they are, this function calls itself<br />

* recursively with the appropriate buffer size as the<br />

* second parameter.<br />

#DEFINE MAX_BUFFER_SIZE 500<br />

* String length at the beginning of the structure<br />

* returned before the UNC path.<br />

* ACC changed to 0 on 9/10/98 - Now using<br />

* WNetGetConnection, which uses a string rather than a<br />

* struct.<br />

#DEFINE STRUCTURE_HEADER 0<br />

local lcReturnValue<br />

if type('tc<strong>Mapped</strong>Path') = "C" and ;<br />

! isnull(tc<strong>Mapped</strong>Path)<br />

Figure 1. Passing<br />

getdir() a UNC<br />

path will initially<br />

display the UNC<br />

path in the dialog<br />

box, but the<br />

unmapped drive<br />

won’t appear in<br />

the drop-down list.<br />

* Split up the passed path to get just the drive.<br />

local lcDrive, lcPath<br />

* Just take the first two characters - we'll put it<br />

* all back together later. If the first two<br />

* characters aren't a valid drive, that's okay. The<br />

* error value returned from the function call will<br />

* handle it.<br />

* Case statement ensures we don't get the "cannot<br />

* access beyond end of string" error.<br />

do case<br />

case len(tc<strong>Mapped</strong>Path) > 2<br />

lcDrive = left(tc<strong>Mapped</strong>Path, 2)<br />

lcPath = substr(tc<strong>Mapped</strong>Path, 3)<br />

case len(tc<strong>Mapped</strong>Path)


lcPath = ""<br />

endcase<br />

declare INTEGER WNetGetConnection IN WIN32API ;<br />

STRING @lpLocalPath, ;<br />

STRING @lpBuffer, ;<br />

INTEGER @lpBufferSize<br />

* Set up some variables so the appropriate call can<br />

* be made.<br />

local lcLocalPath, lcBuffer, lnBufferSize, ;<br />

lnResult, lcStructureString<br />

* Set to +1 to allow for the null terminator.<br />

lnBufferSize = ;<br />

iif(pcount() = 1 or ;<br />

type('tnBufferSize') # "N" or ;<br />

isnull(tnBufferSize), ;<br />

MAX_BUFFER_SIZE, ;<br />

tnBufferSize) + ;<br />

1<br />

lcLocalPath = lcDrive<br />

lcBuffer = space(lnBufferSize)<br />

* Now call the dll function.<br />

lnResult = WNetGetConnection(@lcLocalPath, ;<br />

@lcBuffer, @lnBufferSize)<br />

do case<br />

* String translated sucessfully.<br />

case lnResult = NO_ERROR<br />

* Actually, this structure-stripping is no longer<br />

* required because WNetGetConnection() returns a<br />

* string rather than a struct.<br />

lcStructureString = alltrim(substr(lcBuffer, ;<br />

STRUCTURE_HEADER + 1))<br />

lcReturnValue = left(lcStructureString, ;<br />

at(chr(0), lcStructureString) - 1) + lcPath<br />

* The string pointed to by lpLocalPath is invalid.<br />

case lnResult = ERROR_BAD_DEVICE<br />

lcReturnValue = tc<strong>Mapped</strong>Path<br />

* There's no current connection to the remote<br />

* device, but there's a remembered (persistent)<br />

* connection to it.<br />

case lnResult = ERROR_CONNECTION_UNAVAIL<br />

lcReturnValue = tc<strong>Mapped</strong>Path<br />

* A network-specific error occurred. Use the<br />

* WNetGetLastError function to obtain a description<br />

* of the error.<br />

case lnResult = ERROR_EXTENDED_ERROR<br />

lcReturnValue = tc<strong>Mapped</strong>Path<br />

* The buffer pointed to by lpBuffer is too small.<br />

* The function sets the variable pointed to by<br />

* lpBufferSize to the required buffer size.<br />

case lnResult = ERROR_MORE_DATA<br />

lcReturnValue = getuncpath(tc<strong>Mapped</strong>Path, ;<br />

lnBufferSize)<br />

* None of the providers recognized this local name<br />

* as having a connection. However, the network is<br />

* not available for at least one provider to whom<br />

* the connection may belong.<br />

case lnResult = ERROR_NO_NET_OR_BAD_PATH<br />

lcReturnValue = tc<strong>Mapped</strong>Path<br />

* There's no network present.<br />

case lnResult = ERROR_NO_NETWORK<br />

lcReturnValue = tc<strong>Mapped</strong>Path<br />

* The device specified by lpLocalPath isn't<br />

* redirected.<br />

case lnResult = ERROR_NOT_CONNECTED<br />

lcReturnValue = tc<strong>Mapped</strong>Path<br />

otherwise<br />

lcReturnValue = tc<strong>Mapped</strong>Path<br />

endcase<br />

else<br />

lcReturnValue = tc<strong>Mapped</strong>Path<br />

http://www.pinpub.com FoxTalk December 1998<br />

5<br />

endif<br />

return lcReturnValue<br />

Constants from the corresponding API header<br />

files are reproduced in the function rather than in a<br />

standalone header to make it more portable, and a<br />

couple of local constants are also #DEFINEd for clarity<br />

later in the code.<br />

The mapped drive parameter is checked, and if it’s<br />

not a character expression, or if it’s .NULL., then the<br />

function simply returns whatever was passed to it. If the<br />

tests are passed, then the API function is DECLAREd.<br />

Note the use of the Win32API rather than the specific<br />

MPR.DLL.<br />

Splitting up the mapped path parameter into its<br />

component pieces is simply a matter of taking the first<br />

two characters and calling them the mapped drive, and<br />

taking the rest of the expression and calling them the<br />

path. There’s no problem if that’s not a valid assumption<br />

because the API function will return an error value and<br />

we’ll take the appropriate action.<br />

Next I call the API function and check the return<br />

value. The two important values I’m looking for are<br />

NO_ERROR (my personal favorite ) and<br />

ERROR_MORE_DATA. The NO_ERROR code means<br />

just that: The drive mapping was decoded successfully,<br />

and the result is in the lcBuffer variable.<br />

The value placed in lcBuffer is a null-terminated<br />

string. This means that the string is terminated by a chr(0)<br />

that needs to be stripped before we use it in VFP. I just<br />

append the path part of the original mapped path<br />

parameter to the translated drive returned by the PAI call<br />

and voila—a UNC version of the mapped path.<br />

The ERROR_MORE_DATA return value tells me that<br />

I didn’t allocate enough space in the return buffer, and I<br />

need to call the function again. Fortunately, once I get this<br />

error, I no longer need to guess how long to make the<br />

buffer. In addition to returning the error, the API function<br />

sets lnBufferSize to the value required so I can call<br />

GetUNCPath() recursively with the original mapped<br />

drive path and lnBufferSize.<br />

Each of the other error states indicates that the<br />

mapping couldn’t be decoded for some reason or other.<br />

I’ve decided to treat them all the same way: Simply return<br />

the mapped path passed to the function.<br />

While VFP is a wonderful development environment,<br />

it does have some limitations. Fortunately, it provides<br />

enough access to the Windows API to be able to work<br />

around most of them. ▲<br />

12COASC2.ZIP at www.pinpub.com/foxtalk<br />

<strong>Andrew</strong> <strong>Coates</strong> is an independent developer/data consultant living in<br />

the Olympic City—Sydney, Australia. He specializes in PC database<br />

applications, particularly integrating tools and visualizing spatial data.<br />

a.coates@civilsolutions.com.au.


Best Practices FoxTalk<br />

Seeing Patterns: The Mediator<br />

Jefferey A. Donnici<br />

This month’s column continues the series that looks at some<br />

common design patterns and how they can be found and<br />

used within our Visual FoxPro applications. The pattern<br />

discussed this month is the Mediator pattern, which allows a<br />

single object to handle the interaction between a set of other<br />

objects. In doing so, the set of mediated objects is less<br />

coupled to one another and can be extended or varied<br />

independently. To illustrate the Mediator pattern in action,<br />

two very different examples are given.<br />

WITH the last Best Practices column, I began a new<br />

series called “Seeing Patterns.” For those who<br />

missed that column (for shame!), the intent<br />

behind this series is to discuss the real-world use of some<br />

common design patterns, using VFP examples for<br />

illustration. If you aren’t already familiar with the concept<br />

of object-oriented design patterns, I encourage you to<br />

look through the last column for some references and a<br />

discussion that provides an introduction to the ideas<br />

behind design patterns. Specifically, you should make<br />

sure to get a copy of Design Patterns: Elements of Reusable<br />

Object-Oriented Software by E. Gamma, R. Helm, R.<br />

Johnson, and J. Vlissides (Addison-Wesley, ISBN 0-201-<br />

63361-2). This is the most popular of all the patternsrelated<br />

books, and any developer working with an objectoriented<br />

language should have it.<br />

With the introduction to design patterns out of the<br />

way, I can dive right into this month’s discussion of the<br />

Mediator pattern. This column provides an introduction<br />

to the Mediator and, more importantly, some real-life VFP<br />

examples. Hopefully, the VFP-centric approach to this<br />

discussion will help you learn to “see” this pattern in<br />

your own work, thereby making it easier to pull it from<br />

your “toolbox” when solving a design problem in the<br />

future. Remember, however, that object-oriented design<br />

patterns represent another layer of abstraction above the<br />

design process. As such, the patterns themselves aren’t<br />

specific to any one programming language or design tool.<br />

Why the Mediator?<br />

The purpose of the Mediator pattern is to provide a<br />

central point for interaction between a set of similar<br />

objects. By having all interaction in a set of objects go<br />

through one portion of the component or subsystem, the<br />

dependencies and coupling between the objects in that set<br />

are reduced. As explained in Design Patterns, the intent of<br />

6 FoxTalk December 1998<br />

http://www.pinpub.com<br />

6.0<br />

the Mediator is to “define an object that encapsulates how<br />

a set of objects interact. Mediator promotes loose coupling<br />

by keeping objects from referring to each other explicitly,<br />

and it lets you vary their interaction accordingly.”<br />

A good object-oriented design typically has a large<br />

number of classes in it, simply because behavior and data<br />

are spread out among the classes to provide cohesion. The<br />

more a system is comprised of a set of highly specialized<br />

“pieces,” all working together to provide larger<br />

functionality, the more those pieces can be reused. On the<br />

extreme opposite end of the spectrum, a few large classes<br />

that perform several different functions apiece aren’t<br />

typically reusable in a different application context.<br />

The flip side of this design principle, however, is that<br />

the relationships between (or “the coupling of”) all those<br />

objects brings with it a higher learning curve for the<br />

designer and programmer, as well as the potential for<br />

reduced reusability. If each object in a subsystem must<br />

know the programmatic interface for all the other objects<br />

in the subsystem, then the overall reusability of every<br />

object suffers. So, while a large number of objects in a<br />

system typically means that they’re inherently more<br />

cohesive, each new object in the system has the potential<br />

to increase the number of dependencies exponentially.<br />

The Mediator provides a solution to this problem by<br />

acting as a central point of contact for all those intrasystem<br />

communications. Think of the Mediator as a<br />

“traffic cop” that makes sure everyone gets through the<br />

intersection like they should, without anybody having to<br />

get out of their car and talk to another driver. As long as<br />

all the “mediatees” (the cars and drivers) are being<br />

coordinated by the “mediator” (the traffic cop), there isn’t<br />

any need for the objects (or grumpy drivers) to interact<br />

with one another (see Figure 1).<br />

The benefits of this approach are numerous. The<br />

decoupling of the “mediatees,” called “colleagues” in<br />

Design Patterns, has been mentioned already, but there are<br />

others as well. For example, your class hierarchies can be<br />

less deep because the need to subclass is reduced when<br />

using a Mediator. The Mediator defines the interaction<br />

between the pieces of the system, so only the Mediator<br />

needs to be subclassed when the nature of the<br />

relationships changes. Each of the classes themselves<br />

remains the same, with the “translation” between them<br />

occurring within the Mediator.<br />

Also, the Mediator simplifies the nature of the


elationships between the objects being mediated.<br />

Without the Mediator in place, the communications<br />

between objects would likely take the form of many-tomany<br />

relationships. With the Mediator, those<br />

communications become a series of one-to-many<br />

relationships, which are far easier to understand and<br />

maintain over the long term.<br />

It should be noted, however, that the Mediator does<br />

have one significant drawback. Remember that the goal<br />

of the Mediator is to simplify the interaction between<br />

objects. <strong>Un</strong>fortunately, the Mediator component itself<br />

often becomes fairly complex internally. In Design<br />

Patterns, the point is made that the “Mediator pattern<br />

trades complexity of interaction for complexity in the<br />

Mediator. Because a Mediator encapsulates protocols,<br />

it can become more complex than any individual<br />

colleague. This can make the Mediator itself a monolith<br />

that’s hard to maintain.” While there isn’t any easy way<br />

around this potential problem, it’s best to know in<br />

advance that the Mediator portion of the design might<br />

require special development considerations. Carefully<br />

commenting and documenting the relationships that<br />

have been abstracted from the objects into the Mediator<br />

will go a long way toward lessening the maintenance<br />

cost of the Mediator itself.<br />

Example 1—the Forms Manager<br />

One of the most common examples of the Mediator<br />

pattern—and you might already have this in your own<br />

applications or framework—is a system-wide Forms<br />

Manager. The purpose of the Forms Manager is to provide<br />

a centralized point for communicating with, and between,<br />

the open windows in your application. If, for example,<br />

you need one form to get a value from another form, or if<br />

you want to close all open forms in the application, then<br />

the Forms Manager can handle those requirements. The<br />

following code is a skeleton example of a Forms Manager<br />

class, including some ideas on the functionality and<br />

behavior that it might contain.<br />

DEFINE CLASS cstFormManager AS Custom<br />

PROTECTED aForms[1,3]<br />

PROCEDURE GetFormCount<br />

IF THIS.NoForms()<br />

RETURN 0<br />

ELSE<br />

RETURN ALEN(THIS.aForms, 1)<br />

ENDIF<br />

ENDPROC<br />

PROCEDURE NoForms<br />

RETURN (TYPE('THIS.aForms[1, 1]') == 'L')<br />

ENDPROC<br />

PROCEDURE FindForm<br />

LPARAMETERS tuParam1, tcParamType<br />

*-- The purpose of this method would be to<br />

*-- allow the developer to pass an object<br />

*-- reference, a class name, a caption, or<br />

*-- even an object name as a parameter.<br />

*-- The second parameter would indicate the<br />

Figure 1. The Mediator pattern decouples the objects in a system<br />

so that the relationships between them can be handled in a<br />

centralized fashion.<br />

*-- type of variable passed in the first<br />

*-- parameter. This information would be used<br />

*-- to find the matching form in the<br />

*-- collection. If one is found, the row for<br />

*-- it within the .aForms collection is<br />

*-- returned. Otherwise, 0 is returned.<br />

ENDPROC<br />

PROCEDURE FormExists<br />

LPARAMETERS tuParm1, tcParamType<br />

*-- This method uses the FindForm method to<br />

*-- see whether the indicated form exists.<br />

*-- Instead of returning a row number,<br />

*-- however, it just returns a logical<br />

*-- indicating the existence of the form<br />

*-- being searched for.<br />

RETURN (THIS.FindForm(tuParm1,tcParamType)=0)<br />

ENDPROC<br />

PROCEDURE AddForm<br />

*-- This code is provided for illustration<br />

*-- purposes only. In a production<br />

*-- environment, you would want to provide<br />

*-- more exception handling and perhaps other<br />

*-- "notification" options to indicate that<br />

*-- a new form has been created.<br />

LPARAMETERS toForm<br />

LOCAL lcObjClass,lcObjName,loObject,lnNewRow<br />

*-- If we don't have an object reference, we<br />

*-- can't continue the method. Otherwise,<br />

*-- grab the properties we will<br />

*-- store in the collection.<br />

IF TYPE("toForm") # "O"<br />

RETURN<br />

ELSE<br />

loObject = toForm<br />

lcObjName = loObject.Name<br />

lcobjClass = loObject.Class<br />

ENDIF<br />

*-- If there are no forms, we're putting the<br />

*-- passed form into the first position in<br />

*-- the array.<br />

IF THIS.NoForms()<br />

lnNewRow = 1<br />

ELSE<br />

*-- Otherwise, add a row to the end of the<br />

*-- array to make room for the passed form.<br />

lnNewRow = ALEN(THIS.aForms, 1) + 1<br />

DIMENSION THIS.aForms[lnNewRow,ALEN(THIS.aForms,2)]<br />

ENDIF<br />

THIS.aForms[lnNewRow, 1] = lcObjName<br />

THIS.aForms[lnNewRow, 2] = lcObjClass<br />

http://www.pinpub.com FoxTalk December 1998<br />

7


THIS.aForms[lnNewRow, 3] = loObject<br />

RETURN<br />

ENDPROC<br />

PROCEDURE RemoveForm<br />

LPARAMETERS tuParm1, tcParmType<br />

*-- This method would allow a single form to<br />

*-- be removed from the collection. The<br />

*-- first parameter could be an object<br />

*-- reference, an index position within the<br />

*-- collection, a form name, or even a caption.<br />

*-- When the form is removed, the array<br />

*-- containing the collection would be<br />

*-- adjusted. If the last form in the<br />

*-- collection is removed, the first position<br />

*-- in the collection array would be set to<br />

*-- .F. to indicate an empty collection.<br />

ENDPROC<br />

PROCEDURE RemoveAllForms<br />

*-- This method would allow all forms to be<br />

*-- removed from the collection. The<br />

*-- GetFormCount method would be used<br />

*-- to determine the number of forms in the<br />

*-- collection, and this method would iterate<br />

*-- through them to call the<br />

*-- .RemoveForm method for each.<br />

ENDPROC<br />

PROCEDURE GetFormRef<br />

LPARAMETERS tuParm1, tcParmType<br />

*-- This method returns a reference to the<br />

*-- form indicated by the passed parameters.<br />

*-- The parameter might be a class name, an<br />

*-- index position within the collection,<br />

*-- a caption, or some other form-specific<br />

*-- property. The FindForm method would be<br />

*-- used to find the matching form row<br />

*-- within the collection. If there's no<br />

*-- matching form found, NULL would be<br />

*-- returned.<br />

ENDPROC<br />

ENDDEFINE<br />

The idea behind this class definition is that the Forms<br />

Manager would always be available to the open forms in<br />

the application. This is often handled by making the<br />

Forms Manager itself a member of the omnipresent<br />

“application object.” Because the Forms Manager is<br />

always available, the forms in the application can add<br />

themselves to it when instantiating and remove<br />

themselves from it during the destruction process. For<br />

example, the following code would be placed into a base<br />

form class’s Init() event to automate the process of adding<br />

a new form to the collection:<br />

oApp.oFormsManager.AddForm(THISFORM)<br />

When the form is being closed, the following line<br />

of code in the form’s Destroy() event would prompt<br />

the Forms Manager to remove the closing form from<br />

the collection:<br />

oApp.oFormsManager.RemoveForm(THISFORM)<br />

Obviously, the preceding example code isn’t a<br />

complete Forms Manager solution, but it illustrates a<br />

variety of reasons that a Forms Manager’s functionality<br />

would be beneficial. Because you could get an object<br />

reference to any specific form in the collection, you could<br />

always communicate with the precise form you need in<br />

any situation. If that form isn’t available, the Forms<br />

Manager can indicate this as well so that you can<br />

proceed accordingly.<br />

Example 2—the dialog box<br />

Another problem that the Mediator pattern helps to<br />

solve is the issue of communications between reusable<br />

containers that are conditionally appearing in yet another<br />

container. Most dialog boxes contain interface elements<br />

that are used throughout an application. For example, a<br />

file-selection dialog box contains a directory list box, but<br />

the dialog box is hardly the only need for that control.<br />

Also, dialog boxes usually have controls that turn on/off<br />

or become enabled/disabled, based on the conditions and<br />

state of other controls in the dialog box. For example, if<br />

the user absolutely must choose a file, then the “OK”<br />

button would be disabled until the user has selected a file.<br />

Obviously, dealing with all these layers of<br />

communication can become a problem, especially if the<br />

same control/container appears in a variety of dialog boxstyle<br />

interfaces. Enter the Mediator pattern as an elegant<br />

solution. By providing a single point of interface for all<br />

the controls in the dialog box, the problem of changing<br />

the state of other controls becomes centralized.<br />

The dialog box shown in Figure 2 is a simple example<br />

of a window that contains three different containers. Each<br />

of the containers provides a mechanism for setting a value<br />

to on or off, while only two of them provide a mechanism<br />

for displaying the current state of that value.<br />

The key here is that the dialog box can have any<br />

number of similar containers added to it without a change<br />

to anything else. That is, none of the other controls in this<br />

dialog box would require changes, nor would the dialog<br />

box itself, which is serving as the Mediator component in<br />

this case.<br />

Each of the containers in the window has two<br />

methods that allow it to interact with the dialog box.<br />

These are UpdateMediator() and SetControls(), which<br />

respectively communicate with the mediator to reflect a<br />

change in state and receive messages from the mediator to<br />

update their member controls. For example, the container<br />

with the check boxes contains the following<br />

UpdateMediator() code:<br />

Figure 2. A simplified dialog box that allows for setting a formwide<br />

value either on or off. While three of the containers can set<br />

the value, only two display its current state.<br />

8 FoxTalk December 1998<br />

http://www.pinpub.com


LPARAMETERS tlValue<br />

IF PEMSTATUS(THIS.Parent, "UpdateMediator", 5)<br />

THIS.Parent.UpdateMediator(tlValue)<br />

ENDIF<br />

Each of the check boxes calls this method in its<br />

InteractiveChange() event, passing a logical parameter<br />

according to its role (“on” or “off”). The container then<br />

verifies that its parent container has an UpdateMediator()<br />

method of its own before passing that value up to the<br />

mediator. This allows the member container to be used in<br />

other types of Mediator-style designs, as well as in<br />

designs that don’t require any sort of mediation approach.<br />

The SetControls() method for each container receives<br />

the message from the dialog box and updates the controls<br />

within it. This lets the mediator communicate with each of<br />

its “mediatees” without having to know the specific<br />

internals (the type of control) of those containers. Again<br />

using the check boxes container as an example, here’s the<br />

SetControls() method:<br />

LPARAMETERS tlValue<br />

*-- Note here that I've changed the<br />

*-- check boxes to usual boolean values<br />

*-- instead of numerics.<br />

THIS.chkOn.Value = (tlValue)<br />

THIS.chkOff.Value = (NOT tlValue)<br />

With this functionality in place in each of the<br />

containers that might appear in the dialog box, we can<br />

then look at the requirements of the mediator itself. For<br />

starters, the mediator needs to not only receive the<br />

messages from its “mediatees,” but it also needs to<br />

broadcast interpretations of those messages down to them.<br />

For example, when the user chooses the “off” check box, a<br />

message is sent up to the mediator to indicate that the<br />

state is now “off.” A message then needs to be broadcast<br />

out to the member containers so that they can update<br />

their own internal state to reflect this change.<br />

Here are the contents of the dialog box’s<br />

UpdateMediator() method:<br />

LPARAMETERS tlValue<br />

*-- This is the point at which the<br />

*-- mediator receives the message from<br />

*-- one of its member objects and sends<br />

*-- it to the SetControls method to be<br />

*-- broadcast down to all other member<br />

*-- objects. It's here that the one-to-<br />

*-- many relationship between the mediator<br />

*-- and its colleagues is leveraged.<br />

THISFORM.SetControls(tlValue)<br />

Pretty simple, huh? The truth is that this example is<br />

straightforward because nothing happens to the value<br />

that’s received before it’s transmitted back to the member<br />

containers. It’s a logical value in either case, so we can<br />

simply pass it through to the SetControls() method of the<br />

mediator/dialog box. Here’s the content of that method,<br />

which is only slightly more complex than<br />

UpdateMediator():<br />

LPARAMETERS tlValue<br />

FOR EACH loMember IN THIS.Controls<br />

IF PEMSTATUS(loMember, "SetControls", 5)<br />

loMember.SetControls(tlValue)<br />

ENDIF<br />

ENDFOR && EACH loMember IN THIS.Controls<br />

THISFORM.Refresh()<br />

When the SetControls() method receives the<br />

parameter, it simply loops through its collection of<br />

member controls and broadcasts the value to each of<br />

them. Note that it must check for the existence of the<br />

“SetControls” method before doing the broadcast so that<br />

calling a non-existent method on the “Close” button<br />

doesn’t trigger an error.<br />

“Seeing” the pattern<br />

It’s important to realize that the “mediator” doesn’t<br />

always have to be a single class definition, separate from<br />

the objects being mediated. Remember that the Mediator<br />

describes a pattern to a design solution, not an actual<br />

design. As such, I hope you don’t think of the two<br />

examples I’ve given here, both of which are in this<br />

month’s Subscriber Downloads at www.pinpub.com/<br />

foxtalk, as the only possible types of Mediator designs<br />

you’ll see.<br />

For example, you might have a single container class<br />

that implements a Mediator pattern, using its own<br />

member controls. The preceding example uses different<br />

class definitions to illustrate the potential for reusability<br />

between the mediator and the components it mediates.<br />

However, suppose you have a form with a variety of text<br />

boxes and labels that work together as a type of calculator.<br />

When values are entered into the text boxes, the labels<br />

might be updated to indicate some calculation of those<br />

values. In this case, you might not have a need for<br />

separate container classes. Still, it would be unwieldy<br />

(and require far too much maintenance) for every text box<br />

to know how to update the value for every label on the<br />

form. Instead, you might provide a single method that<br />

each text box calls when it’s been updated. That method<br />

could perform the necessary calculations and update the<br />

label captions accordingly.<br />

My point is to demonstrate that the Mediator pattern,<br />

and indeed all patterns, can exist at a variety of levels of<br />

granularity. While the Forms Manager example is a<br />

system-wide, global implementation of the Mediator<br />

pattern, the dialog box example is smaller in scope and<br />

doesn’t have as wide an effect. A container like the<br />

“calculator” mentioned previously is even more of a<br />

“micro” approach to using the Mediator pattern.<br />

In summary<br />

Hopefully, these VFP examples of the Mediator pattern<br />

got you thinking. I’m pretty certain that most of you will<br />

find examples of the Mediator pattern that already exist in<br />

Continues on page 14<br />

http://www.pinpub.com FoxTalk December 1998<br />

9


Mining for Gold in the FFC<br />

Doug Hennig<br />

Sometimes you have to sift through a lot of rocks before<br />

you find true nuggets of gold. Not so with the FoxPro<br />

Foundation Classes that ship with VFP 6; there’s tons of<br />

gold in them thar hills.<br />

ONE of the design goals for VFP 6 was to make it<br />

easier for programmers new to VFP to get up and<br />

running with the tool. The Application Wizard is<br />

one of the results of this goal, and the FoxPro Foundation<br />

Classes—or FFC—is another. The FFC, located in the FFC<br />

subdirectory of the VFP home directory, is a collection of<br />

class libraries that provide a wide range of functions.<br />

Don’t think that just because previous versions of FoxPro<br />

have included some, shall we say, less useful (to be polite)<br />

example files that these fall into that category. While some<br />

of these do appear to be more demoware than really<br />

useful, there are still lots of great classes in here. It’s well<br />

worth the effort to spend some time looking at these<br />

classes, picking through the rocks to find the nuggets.<br />

The best way to check out the FFC is using a new VFP<br />

6 tool called the Component Gallery, accessible from the<br />

VFP Tools menu. I won’t discuss the features of the<br />

Component Gallery in this article; it’s simple enough to<br />

use that you can navigate your way around just by<br />

playing with it. The FFC classes are displayed in the<br />

Foundation Classes folder of the Visual FoxPro Catalog<br />

(when I refer to where classes can be found later in this<br />

article, I won’t specify this folder or this catalog, just the<br />

subfolder under Foundation Classes). Classes are grouped<br />

by type (for example, Buttons, Dialogs, and Utilities) and<br />

display a description in the status panel when you select<br />

them. Even better, right-clicking on a class displays a<br />

context menu giving you access to the Help topic and<br />

sample files (either to run or to view) for that class. This<br />

makes it very easy to look through the FFC and see which<br />

classes might interest you.<br />

This article will look at some of the FFC classes and<br />

see how we might use them or even subclass them to<br />

make them even more useful. Before we get started,<br />

though, I’ll discuss _BASE.VCX.<br />

Base classes<br />

VFP 6 includes a set of subclasses of VFP base classes in<br />

_BASE.VCX. Although these classes aren’t located in the<br />

Foundation Classes folder in the Component Gallery<br />

(they’re in the My Base Classes folder), this VCX is<br />

Reusable Tools FoxTalk<br />

10 FoxTalk December 1998<br />

http://www.pinpub.com<br />

6.0<br />

located in the FFC subdirectory, and all FFC classes are<br />

subclassed from _BASE classes. If we use FFC classes in<br />

our applications, _BASE.VCX comes along for the ride.<br />

So, a question arises: Although this column has been<br />

developing a robust set of base classes, and many of you<br />

have your own set, should we consider using _BASE<br />

classes for our base classes instead? The reason for even<br />

considering this is, why have two VCXs in our projects<br />

that provide essentially the same thing?<br />

After looking at the _BASE classes, the conclusion<br />

I’ve come to is no; I’ll continue using my own<br />

SFCTRLS.VCX classes. _BASE classes don’t have the<br />

visual changes I’ve made to my base classes—for<br />

example, the AutoSize property for classes such as<br />

_CheckBox and _Label is set at the default .F.; I almost<br />

always want it set to .T., so that’s what I’ve done in<br />

SFCheckBox, SFLabel, and other classes with this<br />

property. Furthermore, _BASE classes also don’t have<br />

the behavior I want—for example, their Error methods<br />

pass the error on to an ON ERROR handler rather than<br />

using the Chain of Responsibility design pattern (up the<br />

class hierarchy and then the containership hierarchy)<br />

that my classes do, as I discussed in my January 1998<br />

column (see “Error Handling Revisited”). Finally, these<br />

classes have a lot of custom properties and methods that<br />

support the Application Wizard. Since I don’t plan to<br />

use the Application Wizard to create applications, these<br />

properties and methods would just complicate things. So,<br />

we’ll just have to live with the fact that _BASE.VCX will<br />

be included in our projects, whether we want it or not, if<br />

we use FFC classes.<br />

_SysToolbars<br />

It’s highly unlikely you’ll want the VFP development<br />

environment toolbars (such as the Standard and Database<br />

toolbars) to be visible when an application is running.<br />

<strong>Un</strong>fortunately, there isn’t a single command that hides<br />

them all, so most developers create an array of toolbar<br />

names, spin through the array and see whether the<br />

current toolbar is visible, and, if it is, hide it. Of course,<br />

you have to do just the opposite when the application<br />

closes down, at least in a development environment.<br />

Because this is a common task, the FFC includes a<br />

class called _SysToolbars; this class is located in<br />

_APP.VCX and appears as “System Toolbars” in the<br />

Application subfolder of the Component Gallery. This


class is very simple; it hides and restores the system<br />

toolbars either automatically or manually. To use it<br />

automatically, either pass .T. to its Init method when you<br />

instantiate it programmatically or set its lAutomatic<br />

property to .T. in the Property Sheet when you drop it on<br />

a form. In this case, any visible system toolbars are hidden<br />

when the object is instantiated and redisplayed when it’s<br />

destroyed. To use it manually, call its HideSystemToolbars<br />

and ShowSystemToolbars methods. You’ll likely want to<br />

instantiate it as soon as possible at application startup<br />

using code similar to this:<br />

oSysToolbars = newobject('_SysToolbars', '_app.vcx', ;<br />

'', .T.)<br />

As long as oSysToolbars stays in scope, the toolbars<br />

stay hidden. You can either manually release this object or<br />

let it go out of scope when the application terminates.<br />

_Resizable<br />

This class, which is defined in _CONTROLS.VCX and<br />

appears as “Resize Object” in the User Controls subfolder<br />

of the Component Gallery, moves and resizes all the<br />

controls in a form when the form is resized. It takes care<br />

of all the mundane details of drilling down through<br />

containers (such as PageFrames), adjusting the Top, Left,<br />

Height, and Width properties of controls. I’ve done this<br />

kind of thing manually before in the Resize method of the<br />

form, writing reams of code to handle all the applicable<br />

controls. Believe me, anything that can automate this<br />

tedious chore is very welcome.<br />

To see how _Resizable works, run the ResizeDemo<br />

form (by the way, this form also contains an _SysToolbars<br />

object with lAutomatic set to .T. so you can see how that<br />

class works). Leave “Resize” unchecked and resize the<br />

form. See how dumb it looks as more background appears<br />

when you make the form larger or controls get hidden as<br />

you make the form smaller? Not the sign of a professional<br />

application. Now check “Resize” (but leave “SFResizable”<br />

unchecked for now); when you resize the form, the edit<br />

box is resized, and the other controls are moved<br />

automatically.<br />

However, there are a couple of problems with this<br />

class. First, it resizes or moves all the controls. Typically,<br />

when you resize a form, you want to resize certain<br />

controls (such as edit boxes) and move the ones below<br />

and to the right of them to account for the new size; the<br />

rest you want to leave alone. Another problem is that it<br />

moves everything proportional to the original size of the<br />

form; the effect is that a resized form looks like a blown<br />

up or shrunk version of the original, with controls further<br />

apart or closer together. The result is sort of like what<br />

happens to writing on a balloon as it’s blown up. In my<br />

experience, what you really want is a form that looks just<br />

like it was but with some of the controls larger or smaller<br />

and the rest just moved relative to the resized controls.<br />

After all, the whole reason for the user to resize a form is<br />

to be able to see more of those controls they’d normally<br />

have to scroll (grids, list boxes, edit boxes, TreeView and<br />

ListView controls, and so forth), not to get a bigger form<br />

with more background.<br />

The good news is that we don’t need to throw<br />

_Resizable out and create a new class with the behavior<br />

we want. _Resizable has all of the logic we need; it just<br />

doesn’t do things quite right. So, let’s subclass it and<br />

make the subclass act the way we expect.<br />

The subclass I created is called SFResizable, and it’s<br />

contained in SFFFC.VCX in the Subscriber Downloads at<br />

www.pinpub.com/foxtalk. I changed both the Height and<br />

Width properties to 17 so the control doesn’t take up as<br />

much space when it’s dropped on a form. Since we need<br />

to treat some controls differently than others (some will be<br />

moved, while others might be resized), we need a way to<br />

indicate how each control should be treated. I originally<br />

considered adding new properties to my base classes (for<br />

example, lResize, which, if .T., would indicate that this<br />

control should be resized) but rejected that because then<br />

SFResizable would only work with a certain set of base<br />

classes, and that would make it far less reusable. Instead, I<br />

decided to make SFResizable self-contained, so I created<br />

the following new properties:<br />

• cRepositionLeftList: A comma-delimited list of the<br />

names of controls that should be moved left-right<br />

only as the form is resized.<br />

• cRepositionList: A comma-delimited list of the names<br />

of controls that should be moved left-right and updown<br />

as the form is resized.<br />

• cRepositionTopList: A comma-delimited list of the<br />

names of controls that should be moved up-down<br />

only as the form is resized.<br />

• cResizeList: A comma-delimited list of the names of<br />

controls that should be resized as the form is resized.<br />

I also overrode the AddToArray and SetSize<br />

methods. AddToArray adds size and position information<br />

about a control to an array property of the class; it’s<br />

called from the LoopThroughControls method, which<br />

processes all of the controls in the form. The problem<br />

with the original AddToArray is that it stores the size and<br />

position information of the control as values proportional<br />

to the size and position of the form rather than the actual<br />

values for the control. So, I simply used the Visual Basic<br />

method of subclassing (I copied the code from<br />

_Resizable.AddToArray and pasted it into SFResizable’s<br />

method) and then modified the code to store the original<br />

values. SetSize is also called from LoopThroughControls;<br />

it adjusts the size and position of a control by the<br />

difference between the original (stored in the<br />

http://www.pinpub.com FoxTalk December 1998<br />

11


InitialFormHeight and InitialFormWidth properties) and<br />

current sizes of the form. Since I don’t want every control<br />

resized and moved, I changed the code to use the<br />

following logic:<br />

• If the control’s name is in the cRepositionList<br />

or cRepositionTopList properties, its Top value<br />

is adjusted.<br />

• If the control’s name is in the cRepositionList<br />

or cRepositionLeftList properties, its Left value<br />

is adjusted.<br />

• If the control’s name is in the cResizeList property,<br />

its Width and, for certain types of controls (Label,<br />

Editbox, Listbox, Grid, PageFrame, Line, OLEControl,<br />

OLEBoundControl, Shape, and Container), Height<br />

values are adjusted.<br />

To use SFResizable, drop it on a form and call its<br />

AdjustControls method in the Resize method of the form.<br />

Enter the names of those controls that should be resized<br />

as the form is resized into the cResizeList property of the<br />

SFResizable object; grids, edit boxes, and other controls<br />

that can scroll are obvious candidates. Enter the names of<br />

those controls that should be moved up-down and leftright<br />

as the form is resized into the cRepositionList<br />

property. For example, controls below and to the right of<br />

an edit box might qualify for this adjustment. For those<br />

that should only be moved up and down, enter their<br />

names into the cRepositionTop property. Examples<br />

include controls below a grid, because these controls need<br />

to move up or down as the grid’s Height is changed.<br />

Finally, enter the names of controls that should be moved<br />

left and right, such as those to the right of an edit box, as<br />

the form is resized into the cRepositionLeft property.<br />

Run the ResizeDemo form again, but this time check<br />

both “Resize” and “SFResizable”. When you resize the<br />

form, the text box beside “Label 1” doesn’t move; the edit<br />

box and shape surrounding it don’t move but are resized;<br />

the “Resize” and “SFResizable” check boxes and the check<br />

boxes beside the edit box move left and right but not up<br />

and down; the text box beside “Label 2” moves up and<br />

down but not left and right; and the “Reset” button<br />

moves both up-down and left-right. In other words, the<br />

form behaves as we’d expect when it’s resized.<br />

A perfect addition to SFResizable would be a builder<br />

to make it easy to specify which controls should be moved<br />

or resized, with perhaps a list of the controls in the form<br />

and check boxes indicating how the selected control<br />

should be treated.<br />

Registry<br />

As you know, the Registry is the “in” place to store<br />

configuration, preference, and other application settings;<br />

INI files are officially passe (although I know lots of<br />

developers who, like me, would rather lead a user<br />

through editing an INI file over the phone than dare<br />

have them use a tool like REGEDIT). Settings should<br />

normally be stored in keys with a path similar to<br />

HKEY_CURRENT_USER\Software\My Company<br />

Name\My Application\Version 1.1\Options.<br />

The FFC Registry class (defined in REGISTRY.VCX<br />

and appearing as “Registry Access” in the Utilities<br />

subfolder of the Component Gallery) is a wrapper class<br />

for the Windows API calls that deal with the Registry.<br />

Rather than having to know that you must open a key<br />

using the RegOpenKey function before you can use<br />

RegQueryValueEx to read the key’s value, all you have<br />

to know with the Registry class is that you call its<br />

GetRegKey method. Here’s an example that gets the<br />

name of the VFP resource file:<br />

#include REGISTRY.H && located in HOME() + 'FFC'<br />

loRegistry = newobject('Registry', 'Registry.vcx')<br />

lcResource = ''<br />

loRegistry.GetRegKey('ResourceTo', @lcResource, ;<br />

'Software\Microsoft\VisualFoxPro\6.0\Options', ;<br />

HKEY_CURRENT_USER)<br />

(Yeah, I know it’s easier to use SET(‘RESOURCE’, 1), but<br />

this is just an example.)<br />

REGISTRY.VCX also contains subclasses of Registry<br />

that make it easier to read and write VFP settings<br />

(FoxReg), ODBC settings (ODBCReg), applications by<br />

file extension (FileReg), and even (horrors!) INI files<br />

(OldINIReg). Registry-specific constants are defined in<br />

REGISTRY.H so you can specify HKEY_CURRENT_USER<br />

(as I did in the preceding code) rather than its value<br />

(-2147483647).<br />

Here are the useful methods in REGISTRY.VCX.<br />

<strong>Un</strong>less otherwise specified, all return a code indicating<br />

success (0, or the constant ERROR_SUCCESS) or the<br />

WinAPI error code; see REGISTRY.H for error codes.<br />

• GetRegKey: Puts the value of the specified key<br />

into a variable.<br />

• SetRegKey: Sets the value of the specified key to the<br />

specified value (the entire key path is created if it<br />

doesn’t exist).<br />

• DeleteKey: Deletes the specified key (and all subkeys)<br />

from the Registry.<br />

• DeleteKeyValue: Removes the value from the<br />

specified key.<br />

• EnumOptions: Populates an array with all the<br />

settings under a specific key and their current values.<br />

• IsKey: Returns .T. if the specified key exists.<br />

Registry can be instantiated at application startup<br />

(perhaps by an Application object) to get the settings the<br />

12 FoxTalk December 1998<br />

http://www.pinpub.com


application needs: location of the data files on a LAN,<br />

names of the most recently used forms (so they can<br />

appear at the bottom of the File menu like Microsoft<br />

applications do), and so forth. It can also be dropped on a<br />

form to save and restore form-specific settings such as the<br />

Left, Top, Height, and Width values (so it has the same<br />

size and position as when this user closed it), grid column<br />

widths and positions (so users can rearrange grids and<br />

have them appear that way the next time), and so on.<br />

Being a picky guy, I have two problems with the<br />

Registry class. First, because the return value of<br />

GetRegKey is a success or error code, you have to pass the<br />

variable you want the value placed into by reference. I’d<br />

prefer it to return the value of the key; after all, if an error<br />

occurs, the WinAPI error code is probably as useful to me<br />

as the “details” section of a GPF dialog, and even if I do<br />

want it, it could easily be stored in a property of Registry<br />

I can query. Second, it’s likely that both the key path<br />

(“Software\Microsoft\VisualFoxPro\6.0\Options” in<br />

the previous example) and the user key (usually<br />

HKEY_CURRENT_USER) are going to be the same for<br />

every method call of a specific instance of a Registry<br />

object. Being the lazy sort, I’d rather put these values into<br />

properties and not pass them every time I call a method.<br />

As with _Resizable, I’ve subclassed Registry into<br />

SFRegistry (also in SFFFC.VCX) to have the behavior I<br />

want. Although I planned to, it turned out that I didn’t<br />

have to add properties for the default key path<br />

(cAppKeyPath) and user key (nUserKey)—they already<br />

existed, even though they’re not used by Registry<br />

(they’re used by the subclasses in REGISTRY.VCX). Since<br />

Registry.Init sets nUserKey to HKEY_CURRENT_USER,<br />

there normally isn’t even a need to change this property.<br />

I changed the GetRegKey, SetRegKey, DeleteKey,<br />

DeleteKeyValue, EnumOptions, and IsKey methods to<br />

accept different parameters than the matching Registry<br />

class methods (I rearranged the parameters so optional<br />

ones are at the end) and return a more appropriate value<br />

(the key value in the case of GetRegKey, the number of<br />

options in the array in the case of EnumOptions, and .T.<br />

if the method succeeded in the case of SetRegKey,<br />

DeleteKey, and DeleteKeyValue). These methods also<br />

store the WinAPI result code into a new nResult property,<br />

which can be used to determine what went wrong if a<br />

method fails. Here’s an example; this is the code from<br />

EnumOptions:<br />

lparameters taRegOptions, ;<br />

tlEnumKeys, ;<br />

tcKeyPath, ;<br />

tnUserKey<br />

local lcKeyPath, ;<br />

lnUserKey, ;<br />

lnReturn<br />

* If the key path and user key weren't passed, use the<br />

* defaults.<br />

lcKeyPath = This.GetKeyPath(tcKeyPath)<br />

lnUserKey = This.GetUserKey(tnUserKey)<br />

* Use the parent class method to enumerate the key,<br />

* store the result code, and return the number of<br />

* options it found.<br />

lnSuccess = dodefault(@taRegOptions, lcKeyPath, ;<br />

lnUserKey, tlEnumKeys)<br />

This.nResult = lnSuccess<br />

lnReturn = iif(lnSuccess = ERROR_SUCCESS, ;<br />

alen(taRegOptions, 1), 0)<br />

return lnReturn<br />

GetKeyPath and GetUserKey are new methods<br />

called by all the overridden methods to either use the<br />

key path and user key values passed, or the defaults<br />

(stored in the cAppPathKey and nUserKey properties)<br />

if they weren’t passed.<br />

Here’s an example of the use of SFRegistry; this code<br />

would be used in the Init method of a form to restore the<br />

size and position it had the last time the user had it open.<br />

This code assumes that an SFRegistry object named<br />

oRegistry was dropped on the form and its cAppPathKey<br />

property was set in the Property Sheet to the key path for<br />

this application.<br />

lcKey = This.oRegistry.cAppPathKey + '\' + This.Name<br />

lcTop = This.oRegistry.GetRegKey('Top', lcKey)<br />

This.Top = iif(isnull(lcTop), This.Top, val(lcTop))<br />

* similar code for Left, Width, and Height<br />

(Since the Registry class only supports reading and<br />

writing strings, VAL() must be used on the return<br />

value. Also, if this is the first time this form is run, a<br />

key might not exist for it in the Registry, in which case<br />

.NULL. is returned, so this code handles that case.)<br />

A method of the form (such as Release) would use<br />

This.oRegistry.SetRegKey to save the current form size<br />

and position in the Registry.<br />

For another example, run REGISTRYDEMO.PRG. It<br />

creates some new keys, displays their values, and shows<br />

how EnumOptions works. (It also deletes the keys it<br />

creates so it doesn’t pollute your Registry permanently.)<br />

_ObjectState<br />

If you’ve been doing your homework on design patterns,<br />

you’re probably aware of a pattern known as Memento.<br />

A Memento is intended to save the state of something,<br />

presumably so it can be restored after it’s been changed.<br />

The FFC includes a class called _ObjectState that’s sort<br />

of like a Memento: It saves the value of one or more<br />

properties of another object and can later restore them to<br />

their former values.<br />

_ObjectState is defined in _APP.VCX and appears as<br />

“Object State” in the Application subfolder in the<br />

Component Gallery. It has an oObject property that<br />

contains an object reference to the object whose state it<br />

maintains; this property can be set by passing the object<br />

reference to the Init method of the _ObjectState object or<br />

by setting it manually. It has a Set method that sets the<br />

value of the specified property of the managed object to<br />

the specified value, optionally first saving the original<br />

http://www.pinpub.com FoxTalk December 1998<br />

13


value in an array property of itself. It also has a Restore<br />

method that restores the value of one or all saved<br />

properties. The Restore method is also called from the<br />

Destroy method, so when the _ObjectState object goes out<br />

of scope, everything is restored automatically. Since the<br />

array property has a single row for each saved property,<br />

you can’t use this class to provide multiple levels of undo;<br />

however, you could subclass _ObjectState and add this<br />

behavior if desired.<br />

Where might we use such a class? One situation<br />

involves things a user can change but might want to<br />

change back. For example, you might allow a user to<br />

rearrange and resize the columns in a grid, but provide a<br />

“Reset to Default” function that puts them back. Another<br />

place would be when you temporarily change the<br />

properties of an object (perhaps so it behaves differently),<br />

do something with the object, and then change them back<br />

again. Rather than having a set of local variables that save<br />

the property values and then having to manually restore<br />

them before the routine ends, you could do something<br />

like this:<br />

local loObjectState<br />

loObjectState = newobject('_ObjectState', '_app.vcx', ;<br />

'', This)<br />

loObjectState.Set('


ActiveX and Automation Review FoxTalk<br />

Integrating ADO and<br />

Visual Basic into Your VFP<br />

Applications, Part 2<br />

John V. Petersen<br />

In Part 1 of this series in the October issue, John introduced<br />

you to some basic (no pun intended ) techniques with<br />

regard to integrating ADO and Visual Basic into your Visual<br />

FoxPro applications. ADO offers unprecedented ways in which<br />

to both access and move data on both an intra- and interapplication<br />

basis. Visual Basic, with its ability to create ActiveX<br />

controls, provides an indispensable mechanism for packing<br />

visual application components that can be reused in any<br />

application—whether it’s written in VFP, VB, or any other<br />

environment capable of hosting ActiveX controls. This month,<br />

John concludes this discussion by expanding and refining the<br />

design started in October.<br />

IN Part 1 of this series in the October issue, I showed<br />

you how to build a Visual Basic ActiveX control that<br />

encapsulated two things:<br />

• The Microsoft OLE-DB DataGrid, version 6.0<br />

• The Microsoft ActiveX Data Objects Library,<br />

version 2.0<br />

The DataGrid itself is an ActiveX control. ADO is a<br />

set of COM objects used to access and update data, which<br />

can be stored in a variety of formats—either relational or<br />

non-relational. For more details on what makes up ADO,<br />

please refer to Part 1 of this series.<br />

The functionality of the ActiveX control was very<br />

simple. ADO RecordSet and Connection objects were<br />

created, and the DataGrid’s DataSource property was<br />

assigned the reference of the newly created RecordSet<br />

object. This allowed the contents of the RecordSet object<br />

to be displayed in the grid.<br />

Why is it necessary to create what’s essentially an<br />

ActiveX control of an ActiveX control? Visual Basic<br />

possesses a couple of important capabilities that VFP<br />

doesn’t currently support natively:<br />

• ActiveX data binding<br />

• The ability to surface COM events<br />

Currently, inside of VFP, an ADO RecordSet can’t be<br />

bound to the DataSource property of a DataGrid ActiveX<br />

control. Further, when ADO objects such as the<br />

http://www.pinpub.com FoxTalk December 1998<br />

15<br />

6.0<br />

Connection and RecordSet are created in VFP, the<br />

properties and methods of those interfaces are exposed.<br />

However, VFP doesn’t have the ability to respond to<br />

events raised by the Connection, RecordSet, or any COM<br />

object created through the CreateObject function. The<br />

only way to have events surfaced in VFP is to create an<br />

ActiveX control.<br />

Limitations with the current solution<br />

Several limitations exist with the current solution:<br />

• Too much functionality—The functionality of creating<br />

the ADO objects and the presentation services offered<br />

by the DataGrid are bundled together. Ideally, these<br />

two items should be split into separate controls to<br />

provide the most functionality. There will be times<br />

when the services of ADO will be required—without<br />

the need to display data in a grid.<br />

• No public interfaces exist to make the data source<br />

configurable—The control is currently hard-wired to<br />

only fetch data from the authors table of the SQL<br />

Server Pubs database.<br />

• Surfaced events are private to the control—These events<br />

need to be surfaced at the ActiveX control interface so<br />

the host application—VFP, in this case—can recognize<br />

the occurrence of these events.<br />

What you need to work through the sample code<br />

Here’s what you need to work through these examples:<br />

• ADO 2.0<br />

• OLE-DB Provider for SQL Server (ships with<br />

ADO 2.0)<br />

• OLE-DB Grid ActiveX control, version 6.0<br />

• SQL Server 6.5 or 7.0<br />

• Visual Basic 5.0 SP 3 or Visual Basic 6.0<br />

• Visual FoxPro 6.0<br />

ADO 2.0 ships with Visual Studio 6. You can<br />

also obtain ADO via a download from http://<br />

www.microsoft.com/data.


Improving the interface<br />

Figure 1 illustrates the new Visual Basic project.<br />

The first order of business is addressing the first<br />

limitation of too much functionality (yes, I know that<br />

sounds funny). The following code is associated with the<br />

grid control. Most of the code deals with the visual<br />

characteristics of the grid—location and size. The most<br />

important method is the SetDataSource Member.<br />

Remember, in VFP, an ADO RecordSet can’t be bound to<br />

the DataGrid control. This is how you can get around that<br />

limitation. Pass the ADO RecordSet to the control, and<br />

have the control do the binding for you within its<br />

boundaries! It works like a charm.<br />

Private Sub UserControl_Initialize()<br />

'When the control is dropped onto the form,<br />

'the grid must be positioned in the upper-<br />

'left corner of the container.<br />

With DataGrid<br />

.Left = 0<br />

.Top = 0<br />

End With<br />

End Sub<br />

Private Sub UserControl_Resize()<br />

'When the container is resized, we want the<br />

'grid to also resize.<br />

With DataGrid<br />

.Width = ScaleWidth<br />

.Height = ScaleHeight<br />

End With<br />

End Sub<br />

'WARNING! DO NOT REMOVE OR MODIFY<br />

'THE FOLLOWING COMMENTED LINES!<br />

'MemberInfo=14<br />

Public Sub SetDataSource(recordset As ADODB.recordset)<br />

Set DataGrid.DataSource = recordset<br />

End Sub<br />

With this design, the services of our grid control<br />

are very granular. Its only purpose is to accept an<br />

ADO RecordSet and display the data. We’ll reserve the<br />

more complicated tasks of responding to events for<br />

another control.<br />

The other control in the Visual Basic project consists<br />

of one user interface element—a Label control. There’s<br />

quite a lot of code behind this control—far too much to<br />

Figure 1. The new Visual Basic ActiveX control project contains<br />

two ActiveX controls.<br />

print in its entirety. However, I’ll highlight the most<br />

important pieces of code.<br />

Going back to the limitations of the first design, no<br />

public interfaces existed to make the ADO DataSource<br />

variable. Also, all of the events in the previous design<br />

were private. This meant that while the control within its<br />

boundaries recognized the event, no mechanism existed<br />

for the control to surface those events to its own interface.<br />

This means that any application environment hosting the<br />

control wouldn’t have the ability to have its own code<br />

execute when those events have fired. After all, that’s<br />

what it’s all about—having the ability to attach VFP code<br />

to ADO events.<br />

The first lines of code in the VB ActiveX control<br />

are as follows:<br />

Dim WithEvents oconn As ADODB.Connection<br />

Dim WithEvents ors As ADODB.recordset<br />

This provides that ability to the ActiveX control to<br />

respond to events that each of these objects will fire. The<br />

previous design took advantage of this ability and<br />

attached Visual Basic code to those events. In the old<br />

design, when an ADO connection was established, a<br />

MessageBox dialog box appeared, stating that the ADO<br />

connection was complete. While not very useful, it served<br />

to illustrate how events could be trapped. Visual Basic<br />

code is nice, but we work in VFP. We need the ability to<br />

surface these events so that VFP code can be used.<br />

Custom events<br />

Remember when VFP 3.0 was released back in 1995? We<br />

were given the ability to define our own methods and<br />

properties. Defining events, on the other hand, was<br />

something that wasn’t—and still isn’t—-provided. I, for<br />

one, didn’t see the big deal in this. After all, there are<br />

hundreds of events already defined. Why would I need<br />

more? As you’ll see, the ability to define custom events is<br />

a very powerful capability.<br />

To illustrate the power of events, I’ll focus on the<br />

MoveComplete event of the ADO RecordSet object. This<br />

event fires whenever the record changes. This event gets<br />

fired as a result of invoking the MoveFirst, MovePrevious,<br />

MoveNext, or MoveLast methods. The MoveComplete<br />

event also fires when an ADO RecordSet is first opened.<br />

Before looking at the custom event, let’s take a look at<br />

the code for the internal event that gets fired:<br />

Private Sub ors_MoveComplete( _<br />

ByVal adReason As ADODB.EventReasonEnum, _<br />

ByVal pError As ADODB.Error, _<br />

adStatus As ADODB.EventStatusEnum, _<br />

ByVal pRecordset As ADODB.recordset)<br />

RaiseEvent movecomplete(adReason, pError, _<br />

adStatusCancel, pRecordset)<br />

End Sub<br />

In this code, the internal ors_RecordSet object<br />

variable will respond to the MoveComplete event. This<br />

code block, in turn, will fire. The code to key on is the<br />

RaiseEvent statement. Notice that another MoveComplete<br />

16 FoxTalk December 1998<br />

http://www.pinpub.com


event is being fired, and the arguments passed to the<br />

internal event are being passed as well. What’s going<br />

on here?<br />

In this new design, a public event has been created<br />

for each of the internal events. To keep things consistent,<br />

the same name was used. The following line of code in the<br />

Visual Basic control defines the event:<br />

Event MoveComplete( _<br />

ByVal adReason As ADODB.EventReasonEnum, _<br />

ByVal pError As ADODB.Error, _<br />

adStatus As ADODB.EventStatusEnum, _<br />

ByVal pRecordset As ADODB.recordset)<br />

There are 11 events for the ADO RecordSet object. The<br />

new ActiveX control has 11 custom events that correspond<br />

to each to each of the RecordSet events that will get fired<br />

internally. In addition to surfacing events, three public<br />

properties were created as well. These properties are:<br />

• Provider—This specifies the name of the OLE-DB<br />

provider to use.<br />

• ConnectionString—This specifies the connection<br />

string used to create an active ADO connection.<br />

• RecordSetSource—This specifies the source of the<br />

data contained in an ADO RecordSet. This source<br />

could be the name of a table or a SQL statement.<br />

Finally, two public methods exist that do the work of<br />

creating both the ADO connection and RecordSet objects.<br />

These methods are:<br />

• CreateConnection—This method uses the Provider<br />

and ConnectionString properties to establish an<br />

ADO connection.<br />

• CreateRecordSet—This method uses both the<br />

Connection object created by the CreateConnection<br />

Figure 2. The new Visual Basic ActiveX controls must be<br />

registered for use in VFP.<br />

method and the RecordSetSource property to<br />

establish the ADO RecordSet object.<br />

With a new design, it’s time to host the controls<br />

in VFP.<br />

Hosting the controls in VFP<br />

Before you can use the controls, they must be registered in<br />

VFP. Figure 2 shows how the entries will appear in the<br />

Tool\Options\Controls dialog box.<br />

Figure 3 illustrates the VFP form used to host these<br />

new controls.<br />

The following code in the Init event of the form<br />

initializes the controls to both get and display data:<br />

Local connstring<br />

connstring = "Persist Security Info=False;"<br />

connstring = connstring ;<br />

+ "User ID=sa;Initial Catalog=pubs;"<br />

connstring = connstring + "Data Source=(local)"<br />

With This.vbADO<br />

.Provider = "SQLOLEDB.1"<br />

.ConnectionString = connstring<br />

.RecordSetSource = "authors"<br />

.CreateConnection<br />

.CreateRecordset<br />

EndWith<br />

With This<br />

.ors = This.vbADO.adorecordset<br />

.oconn = This.vbADO.adoconnection<br />

EndWith<br />

This.vbgrid.SetDataSource(This.ors)<br />

Figure 4 illustrates the VFP form in action.<br />

So then, where do the events fit in? The following<br />

code is contained in the MoveComplete event of the<br />

vbADO ActiveX control:<br />

*** ActiveX Control Event ***<br />

LPARAMETERS adreason, perror, adstatus, precordset<br />

Thisform.tmrRefresh.Enabled = .T.<br />

Whenever the MoveComplete event fires,<br />

the tmrRefresh object on the form is enabled. The<br />

following code is contained in the Timer event of the<br />

Figure 3. The new Visual Basic ActiveX controls hosted by<br />

a VFP form.<br />

http://www.pinpub.com FoxTalk December 1998<br />

17


tmrRefresh object:<br />

ThisForm.Refresh<br />

This.Enabled = .F.<br />

To help make the point clearer, the following is the<br />

code for the cmdNext CommandButton:<br />

ThisForm.ors.MoveNext<br />

Did you notice that a call to the Refresh method of the<br />

form isn’t present in the CommandButton? The same is<br />

true for the other navigation buttons as well. What does<br />

this mean? It means that code only has to exist in one<br />

location. The alternative is to have method calls in<br />

multiple locations. Since the COM events aren’t surfaced<br />

in VFP, this is usually a required course of action. With the<br />

ability to both recognize events and attach VFP code to<br />

those events, refresh code only has to be written once<br />

and, most importantly, only called from one location: the<br />

MoveComplete event. What’s the benefit? Whether you<br />

navigate via a CommandButton or the DataGrid control,<br />

the MoveComplete event will fire. The form will refresh.<br />

Clearly, this is a much better design—especially from a<br />

code maintenance standpoint.<br />

At this point, you might be asking, why the Timer<br />

Control? I had problems calling the Refresh method<br />

directly in the event. To avoid errors and get the correct<br />

functionality, the Refresh method had to be called after<br />

the MoveComplete event fired. At this point, I don’t<br />

know what’s causing the problem. I’ll be reporting this<br />

to the VFP team at Microsoft and will report back on<br />

their response.<br />

A quick word on the _VFP AutoYield property<br />

By default, the AutoYield property of the _VFP object is<br />

set to .T. This means that VFP will automatically process<br />

pending Windows events that occur between the<br />

execution of VFP program code. For this solution to work<br />

properly, you must be sure that the AutoYield Property is<br />

set to .F. Otherwise, the events will never be surfaced in<br />

VFP. This task is taken care of in the Load event of the<br />

form. The AutoYield setting is reset to the default in the<br />

Destroy event.<br />

Conclusion<br />

Once again, the joining of VFP, Visual Basic, and ADO<br />

can prove to be a powerful combination. Using a Visual<br />

Basic-created ActiveX control provides a way to bring<br />

functionality that doesn’t exist in VFP. For this reason,<br />

it’s more important than ever to go beyond VFP and give<br />

serious consideration to the other tools Visual Studio<br />

has to offer.<br />

Language aside, this article brings to light issues<br />

to ponder when considering a design. Clearly, when<br />

possible, make use of events to centralize code—as<br />

opposed to making redundant method calls. From a<br />

maintenance standpoint, you’ll definitely save yourself<br />

Figure 4. The live VFP form implementing the two<br />

ActiveX controls.<br />

a lot of work.<br />

Finally, I want to take this opportunity to thank my<br />

writing partner and friend Rod Paddock for being a great<br />

sounding board for this article. This is definitely new and<br />

uncharted territory. Rod’s assistance was invaluable when<br />

I needed to negotiate the rough spots. ▲<br />

12PETERS.ZIP at www.pinpub.com/foxtalk<br />

John V. Petersen, MBA, is vice president of IDT Marketing Systems and<br />

Services, a Philadelphia-based marketing database consulting firm. John<br />

has presented at numerous developer conferences, including DevCon 97,<br />

Tech-Ed, and the Southwest Visual FoxPro Conference. John is a Microsoft<br />

Most Valuable Professional (MVP) and a co-author of Developing Visual<br />

FoxPro 5.0 Enterprise Applications and Hands-On Visual Basic 5—Web<br />

Development, both from Prima Publishing. j_petersen@idtmarketing.com.<br />

18 FoxTalk December 1998<br />

http://www.pinpub.com


Second Star on the Right<br />

and Straight on ’til Morning<br />

Paul Maskens and Andy Kramek<br />

This month, Paul and Andy take a look at naming and<br />

object referencing.<br />

Paul: I was reading through threads on CompuServe<br />

recently and picked up a few ideas from there, things that<br />

seem to be causing people trouble, that we can deal with<br />

here. There appears to be a general confusion about where<br />

to put the code in a VFP application and how to refer to<br />

the user interface controls in that code. The power and<br />

flexibility of VFP doesn’t help either, as there’s no one<br />

right way to do things.<br />

Andy: Let’s start with naming and referencing in this<br />

column. We’ll worry about where to put the code next<br />

time. The first thing is to give things meaningful names.<br />

Imagine a form with 25 text boxes named Text1 to Text30<br />

randomly (with some numbers missing, of course).<br />

Paul: I don’t have to imagine it, I’ve seen it! Not only that,<br />

but every form in the application had a Name of “Form1,”<br />

which didn’t make life any easier.<br />

Andy: So here’s a guideline for you, then. Name your<br />

forms with the same name as their SCX (that has to be<br />

unique within the application anyway), and add to your<br />

basic form class a Label named lblFormName that has in its<br />

INIT() method THIS.caption = THISFORM.Name. Now<br />

whenever you run your form, you have a little label<br />

showing the actual name of the form, and when the user<br />

encounters an error, he or she can tell you which form the<br />

error was in, with a name that means something to you.<br />

Paul: You’re presuming that we use a form class, of<br />

course. (We ought to return to that point some time, too.)<br />

For now, can we just assume things are simple? Even<br />

using VFP base classes, as many beginners do.<br />

For example, take a form with a text box and a<br />

Command button. We’re obviously agreed that the first<br />

thing is to give them names so you can refer to them.<br />

Accepting the VFP default of Form1, Text1, and<br />

Command1 isn’t going to be very helpful in the long<br />

term! So once they have names like frmCustomer,<br />

txtName, and cmdSearch, the function of this form and<br />

the controls on it begin to be self-explanatory.<br />

The Kit Box FoxTalk<br />

Andy: I would emphasize that using names isn’t a<br />

substitute for adding comments, no matter how clear<br />

those names are!<br />

http://www.pinpub.com FoxTalk December 1998<br />

19<br />

6.0<br />

Paul: Yes! There’s a much under-used form property<br />

named Comment in the VFP base class. Like the Tag<br />

property, this property is never addressed directly by VFP<br />

itself. It’s entirely for developer use. Typically, Comment<br />

is for, well, comments, and Tag is for data.<br />

I want to keep this simple, so let’s have the button<br />

change the background color of the text box. Now we<br />

have a form named frmDemo. No, er, it’s the first demo of<br />

many. So let’s try frmDemo1. Whoops, we’re back to the<br />

Text1-Text30 problem described earlier. Uh, how about<br />

frmColourChangingDemo?<br />

Andy: See, naming things is harder than writing the code!<br />

Just call it frmColChg—that means we don’t need to argue<br />

about how to spell “color.”<br />

Paul: Okay, so following on from that, we could have<br />

cmdChgCol to change the color, and txtShoCol that shows<br />

the color change. The code is really easy now. In the<br />

Click() event of cmdChgCol, it’s just one line of code:<br />

THISFORM.txtShoCol.backcolor = RGB(255,0,0).<br />

Andy: That’s fine. It’s an example of indirect referencing<br />

that will work as long as both objects are on the form.<br />

Now how about making this very useful tool reusable by<br />

creating a class and putting the class on the form instead<br />

of the individual controls. Just select both of the controls<br />

and choose Save As Class from the File menu. Name the<br />

class and the library to store it in. Then delete the controls<br />

and add an instance of the new class to the form.<br />

Paul: What about the name of this new class? And<br />

what’s the name of this class’s instance on the form? I<br />

reckon cntChgCol is pretty good for the class, and to make<br />

matters a little confusing, I think it’s a good name for the<br />

instance, too.<br />

Oh! Once I did that, everything fell apart, though.<br />

Running the form produces an “<strong>Un</strong>known member<br />

‘txtShoCol’ (Error 1925)”. I know it’s there—I can see it,<br />

and its name is still txtShoCol in the form designer. But


there’s now a container in the way, so there isn’t a<br />

txtShoCol object on the form anymore. Instead it’s<br />

contained in the cntChgCol, which is on the form, so the<br />

reference now has to be THISFORM.cntChgCol.txtShoCol<br />

instead.<br />

Andy: Or even better, use THIS.Parent.txtShoCol instead.<br />

In fact, that’s what you probably should have done the<br />

first time, planning ahead in case the group of controls<br />

was going to be reused in a container class, or you were<br />

going to add them to a Page Frame to the form.<br />

Paul: That reminds me—in MS Knowledge Base article<br />

Q155013, there’s a text box in a column of a grid in a<br />

container that’s doing an incremental search. In the<br />

KeyPress event, there’s a line of code that seems a<br />

little excessive:<br />

THIS.Parent.Parent.Parent.Search( nKeyCode )<br />

I can work out what it’s doing—the Search() method<br />

is a method of the container, and this is passing the<br />

keystroke to it, but that’s not very clear, is it? It’s unlikely<br />

to be broken when it’s reused, because the article is about<br />

building a custom control.<br />

Andy: So what are you suggesting? In that particular case,<br />

I don’t see that there’s any alternative. The referencing has<br />

to be relative, because the text box can’t possibly know<br />

what the name of the outermost container will be at<br />

runtime. There’s no “THISCONTAINER” reference, and<br />

anything relative to “THISFORM” is obviously not<br />

applicable. It’s not pretty, but I don’t see what else you<br />

can do.<br />

Paul: Well, what I’d do is create a ThisContainer reference.<br />

Either as a property if I’m using a class, which is set to<br />

THIS.Parent.Parent.Parent in the INIT() method, or as a<br />

LOCAL variable that has to be set every time the<br />

KeyPress method is called (and therefore is of little<br />

benefit). There’s a marginal benefit in the latter<br />

approach, because the reference is also used in<br />

THIS.Parent.Parent.Parent.Pop() in the same method<br />

where the variable can be used as well.<br />

Andy: I see. So you’re proposing to replace this:<br />

IF nKeyCode = 13 && They hit the ENTER key<br />

This.Parent.Parent.Parent.Pop()<br />

ENDIF<br />

IF (nKeyCode > 48 AND nKeyCode < 58) OR ;<br />

(nKeyCode > 64 AND nKeyCode < 123)<br />

** Calls the search method if you hit a<br />

** letter or numeric key.<br />

This.Parent.Parent.Parent.Search(nKeyCode)<br />

ENDIF<br />

with this:<br />

LOCAL loThisContainer<br />

loThisContainer = This.Parent.Parent.Parent<br />

IF nKeyCode = 13 && They hit the ENTER key<br />

loThisContainer.Pop()<br />

ENDIF<br />

IF (nKeyCode > 48 AND nKeyCode < 58) OR ;<br />

(nKeyCode > 64 AND nKeyCode < 123)<br />

** Calls the search method if you hit a<br />

** letter or numeric key.<br />

loThisContainer.Search(nKeyCode)<br />

ENDIF<br />

As you say, the speed benefit is marginal, but the<br />

code is now much more readable and hence easier to<br />

maintain. I’m wondering why use a LOCAL variable<br />

if you can’t create a property, when you could use a<br />

PUBLIC variable to simulate the property and initialize<br />

it by placing goThisContainer = THIS in the container’s<br />

INIT() method.<br />

Paul: Aaargh! Splutter, cough . . . You’re playing devil’s<br />

advocate, aren’t you? Well, since there can be only one<br />

global variable, you’ll run into problems if your forms are<br />

modeless and can have multiple instances open in the<br />

application at a time—or even if you adopt this technique<br />

in every container class and just have two or more<br />

containers on the same form.<br />

Andy: All right! I give in; even though my forms would<br />

probably be modal anyway, the second reason is the<br />

clincher. (Just testing .)<br />

20 FoxTalk December 1998<br />

http://www.pinpub.com


Paul: I could have used WITH ... ENDWITH instead of<br />

creating a variable, like this:<br />

WITH This.Parent.Parent.Parent<br />

IF nKeyCode = 13 && They hit the ENTER key<br />

.Pop()<br />

ENDIF<br />

IF (nKeyCode > 48 AND nKeyCode < 58) OR ;<br />

(nKeyCode > 64 AND nKeyCode < 123)<br />

** Calls the search method if you hit a<br />

** letter or numeric key<br />

.Search(nKeyCode)<br />

ENDIF<br />

ENDWITH<br />

But that doesn’t make it any more legible. By the time<br />

I’m reading that second .Search() method call, it’s easy to<br />

forget where that method is. Besides, I find that leaving<br />

off the leading period is the smallest, hardest to find, most<br />

catastrophic typing mistake that I regularly make.<br />

Andy: So can we derive a general rule here? How about<br />

this: Multiple layers of referencing should be resolved as few<br />

times as possible, preferably only once.<br />

Paul: I’d add that in doing so you should keep the<br />

code readable, either by commenting it properly<br />

(Andy: shocked gasp!) or using a self-documenting<br />

object reference.<br />

Andy: That’s all very well for your simple example, but in<br />

real life things are a lot more complicated. In order to<br />

send messages between objects, you have to know their<br />

relative positions in the object hierarchy so that you can<br />

work out the addressing properly.<br />

Paul: What exactly do you mean by the “object<br />

hierarchy”? I’m familiar with the “class hierarchy,”<br />

which defines the inheritance relationship, but I’m not<br />

quite sure what you mean by the object hierarchy.<br />

Andy: Well, I guess another name for it is the<br />

“containership hierarchy”—though I don’t really like the<br />

term because it implies that it deals with “containers”<br />

specifically, whereas we’re talking about all objects,<br />

whether they’re in containers or not. It refers to the<br />

relative positions of objects within the principal container<br />

(typically the Form) and describes the relationships<br />

between them. In order to reference one object from<br />

another, you need only determine the position of each in<br />

the hierarchy to be able to work out how to pass a<br />

message between the two. This is easier to illustrate than<br />

to explain, so take a look at the form shown in Figure 1.<br />

Paul: Okay, I call it the containership hierarchy because I<br />

usually need to use it when I’m messing with containers.<br />

That form looks pretty standard (though I wouldn’t have<br />

laid it out that way ); what’s your point?<br />

Figure 1. Sample contact manager form.<br />

Andy: Well, this form is constructed from a mixture of<br />

classes and individual objects, so the messaging gets<br />

pretty complex. For example, the “Personal Mobile Phone<br />

Number” field is named txtPersMobi, but it’s not a direct<br />

member of the form. It’s actually contained in an object<br />

derived from the class cntFullAdd, which is itself a<br />

composite class made up of two other classes—cntAddress<br />

and cntContact—and the field in question is defined as a<br />

member of the latter.<br />

Paul: Hmm, I think I see what you’re getting at. The<br />

address to get that field’s value is actually going to be:<br />

ThisForm.cntFullAdd.cntContact.txtPersMobi.Value<br />

But it’s not exactly obvious, is it?<br />

Andy: Precisely! Especially when you consider that the<br />

Find button actually is a direct member of the form (its<br />

apparent container is actually just a “shape” object),<br />

whereas the Save button is a member of the cntFormMode<br />

class. Okay, I admit this is a pretty contrived illustration,<br />

but I’ve seen more errors and “bugs” in my own (and<br />

other people’s) code because the object hierarchy isn’t<br />

properly understood than I care to think of.<br />

Paul: Oh, I don’t know that it’s all that contrived. I’ve seen<br />

plenty of examples of forms that mix things up like this,<br />

and yes, I agree, it’s always a problem trying to work out<br />

how things relate—especially if you didn’t do the work<br />

yourself in the first place. I assume you have a solution,<br />

since you’re raising the question?<br />

Andy: Yes, I have. It’s really terribly simple and yet again<br />

illustrates how object-oriented programming reflects the<br />

real world. See if you can guess it—after all, what we’re<br />

talking about is how to ensure that someone who wants to<br />

navigate within your form doesn’t get lost. What would<br />

http://www.pinpub.com FoxTalk December 1998<br />

21


you do to ensure that I don’t get lost when I’m coming to<br />

your new offices for the very first time?<br />

Paul: Easy, draw you a map!<br />

Andy: Give the man a cigar! So why don’t we apply this<br />

solution to this case? Figure 2 shows a map of the object<br />

hierarchy for the form shown previously.<br />

Paul: That’s pretty neat. It’s just a standard Organization<br />

Chart, isn’t it? It looks rather like the Object model<br />

diagrams that MS produces for what used to be OLE<br />

Automation (I can’t remember what it’s called this week).<br />

Andy: Yes, it was produced using the MS Organization<br />

Chart 2.0 add-on that ships with Office, and you can use<br />

it to work out how to reference things correctly. All you<br />

need to do is start “walking” from the initial object<br />

(“This”) toward the object you want to address. Each<br />

time you meet a shaded object, you must add that to<br />

the address—using “Parent” if you’re climbing up the<br />

tree, and the object’s name if you’re<br />

going down.<br />

Paul: Okay—let’s suppose I want the<br />

Save button to read the person’s<br />

“Known As” name. I start from the<br />

button with ‘This’, then move<br />

upwards and find myself at the<br />

cntFormMode object, which is shaded,<br />

so I add a parent reference to get<br />

‘This.Parent’.<br />

Carrying on upwards, I reach the<br />

form object (also shaded, so I need<br />

another “parent”) and get<br />

‘This.Parent.Parent’ before starting<br />

downwards again to arrive at<br />

cntName—which just gets added:<br />

‘This.Parent.Parent.cntName’. Finally,<br />

I reach the target text box, so my full<br />

address (for the Value property) is<br />

‘This.Parent.Parent.cntName<br />

.txtKnownAs.Value’.<br />

Figure 2. Object hierarchy diagram.<br />

Andy: Correct. Of course, you can<br />

also take a shortcut, since you can see<br />

from the object hierarchy that you<br />

have to get all the way up to the form<br />

before you can start downwards<br />

again toward your objective. So<br />

you can simply start directly<br />

from the form using “ThisForm”,<br />

which would give you the<br />

address ‘ThisForm.cntName<br />

.txtKnownAs.Value’. Figure 3. Improved object hierarchy diagram.<br />

Paul: There’s one thing your diagram doesn’t make<br />

clear—why can’t I go directly from cntFormMode to<br />

cntName without passing through the form? There’s a line<br />

between them, isn’t there?<br />

Andy: No, there isn’t. Remember, we’re dealing with a<br />

hierarchy, so you can only go up or down. In reality,<br />

there’s no direct connection between the two containers—<br />

it’s just drawn that way for simplicity. Each object in the<br />

top row is actually connected directly, and only, to the<br />

form. “All roads lead to Form” . Similarly, to get from<br />

one text box in a container to another in the same<br />

container, you must go up to the container level before<br />

starting back down again. I suppose a more correct<br />

representation of the situation would look like Figure 3<br />

(I’ve added a few other objects so that you can see<br />

how they fit).<br />

Paul: Yes, that’s quite clear now. I do things slightly<br />

differently. Because I don’t always work these things out<br />

in advance, what I often do is print out the form and then<br />

22 FoxTalk December 1998<br />

http://www.pinpub.com


write on it. I keep printouts of the form anyway for<br />

documentation, so an extra one for my reference is no<br />

great effort. While I’m in the form designer, I just press<br />

PrtSc on the keyboard, Alt-Tab to paintbrush, and<br />

Ctrl-V to paste in the screenshot, then print it landscape.<br />

Then I get out the four colored pens and draw boxes<br />

around the containers, write in their names, and write the<br />

names of controls. Then I might even write in the names<br />

of methods too, because there are occasions when the<br />

interface design is done with the customer before the rest<br />

of the design is finalized.<br />

Andy: That’s a useful little technique; I’d better try<br />

that out. Doing it in the designer means that you’ll get<br />

the names of all but the very smallest objects as part<br />

of the printout, too—nice! (I’ve always done it using<br />

a proprietary screen capture tool with the form<br />

running; I think I forgot about the PrtSc key when I<br />

gave up DOS).<br />

Paul: So do you want to summarize this whole business<br />

neatly now?<br />

Andy: I think so; let’s see. When Wendy Darling asked<br />

Peter Pan how to get to Never-Never Land, he drew her a<br />

mental map and used names that meant something to her.<br />

We need to follow the same approach when working with<br />

forms and containers. Name the objects meaningfully,<br />

and use some sort of visualization of the entire object<br />

hierarchy to ensure that we address objects properly.<br />

Doesn’t seem too difficult, really, does it?<br />

Paul: Nope, not difficult at all. One small difference,<br />

though—in FoxPro, if you don’t have a map, your<br />

program ends up in Never-Never Land. While you’re<br />

there, if you see a bag of marbles about, they’re mine!<br />

(clap, clap, clap, clap, clap, I do believe in FoxPro!) ▲<br />

Paul Maskens is a VFP specialist and FoxPro MVP who works as<br />

systems engineer for AZURE IT Ltd., based in Oxford, England.<br />

pmaskens@compuserve.com.<br />

Andy Kramek is an old FoxPro developer and FoxPro MVP who<br />

works as an independent contractor based in Birmingham, England.<br />

andykr@solihull.bytenet.co.uk.<br />

Downloads December Subscriber Downloads<br />

• 12COASC2.ZIP—Source code described in <strong>Andrew</strong> <strong>Coates</strong>’s<br />

article, “<strong>Un</strong>-<strong>Mapping</strong> <strong>Mapped</strong> <strong>Network</strong> <strong>Drives</strong>.”<br />

• 12DONNSC.ZIP—Source code described in Jefferey<br />

Donnici’s article, “Seeing Patterns: The Mediator.”<br />

• 12DHENSC.ZIP—Source code described in Doug Hennig’s<br />

article, “Mining for Gold in the FFC.”<br />

• 12PETERS.ZIP—Source code described in John Petersen’s<br />

article, “Integrating ADO and Visual Basic into Your VFP<br />

Applications, Part 2.”<br />

Extended Articles<br />

• 12SETTI.HTM—Stephen Settimi’s article, “Creating a Set of<br />

Business Rules and the Making of a BusinessRule Server.” In<br />

this article, Stephen first shows how to visualize a business<br />

object as a collection of smaller objects. Once the objects<br />

are clearly identified and defined (the latter isn’t covered in<br />

Earn Money!<br />

. . . and see your name in print<br />

this article), it’s time to set out those business objects that<br />

might need rules, define those rules, collect them, and then<br />

execute them through a BusinessRule Server and maintain<br />

them as discrete object components.<br />

• 12SETTI.ZIP—Source code described in Stephen Settimi’s<br />

article, “Creating a Set of Business Rules and the Making of a<br />

BusinessRule Server.”<br />

• 12BOOTH.HTM—Jim Booth’s article, “What’s Really Inside:<br />

To Open the Tables or Not to Open the Tables—That is<br />

the Question.” Have you ever wondered if there’s any benefit<br />

to opening the tables for a SQL SELECT command before<br />

you execute the SELECT? Does the SELECT use the source<br />

tables that are already open? Interesting questions. Here<br />

are the answers.<br />

• 12BERG.HTM—“To PrintScreen or Not to PrintScreen.” Art<br />

Bergquist’s follow-up to his September article, “The Visual<br />

FoxPro Screen Image Printer.”<br />

Go ahead, add a line to your résumé—“Published Articles.”<br />

If your tip shows up in the pages of FoxTalk,<br />

we’ll send you $25. See the back page for the<br />

address where you can send your tips.<br />

http://www.pinpub.com FoxTalk December 1998<br />

23


Online Subscription Options Available!<br />

Electronic Access Package<br />

Move closer to your dream of the paperless office by<br />

subscribing to our electronic access package. This option<br />

includes unlimited online access to each monthly issue of<br />

the newsletter (including Subscriber Downloads) for the<br />

term of your subscription as well as the entire FoxTalk<br />

archive (1996-present). The search capability helps you find<br />

answers fast. To subscribe, go to www.pinpub.com/foxtalk/<br />

subscrib.htm or call 1-800-788-1900 to speak to a customer<br />

service representative.<br />

Deluxe Package<br />

This option includes regular delivery of your print copies of<br />

the newsletter as well as online access to both the newsletter<br />

and Subscriber Downloads content. You’ll be able to access<br />

each monthly issue on the Web before it even hits the mail as<br />

FoxTalk Subscription Information:<br />

1-800-788-1900 or http://www.pinpub.com<br />

Subscription rates:<br />

<strong>Un</strong>ited States: One year (12 issues): $179; two years (24 issues): $259<br />

Canada:* One year: $194; two years: $289<br />

Other:* One year: $199; two years: $299<br />

Single issue rate: $17.50 ($20 in Canada; $22.50 outside North America)*<br />

European newsletter orders:<br />

Tomalin Associates, <strong>Un</strong>it 22, The Bardfield Centre,<br />

Braintree Road, Great Bardfield,<br />

Essex CM7 4SL, <strong>Un</strong>ited Kingdom.<br />

Phone: +44 1371 811299. Fax: +44 1371 811283.<br />

E-mail: 100126.1003@compuserve.com.<br />

* Funds must be in U.S. currency.<br />

Australian newsletter orders:<br />

Ashpoint Pty., Ltd., 9 Arthur Street,<br />

Dover Heights, N.S.W. 2030, Australia.<br />

Phone: +61 2-9371-7399. Fax: +61 2-9371-0180.<br />

E-mail: sales@ashpoint.com.au<br />

Internet: http://www.ashpoint.com.au<br />

The Subscriber Downloads portion of the FoxTalk Web site is available to paid<br />

subscribers only. To access the files, go to www.pinpub.com/foxtalk, click on<br />

“Subscriber Downloads,” select the file(s) you want from this issue, and enter the<br />

user name and password at right when prompted.<br />

well as obtain unlimited access to the entire FoxTalk archive<br />

(1996-present). The search engine makes looking for<br />

information on any topic a breeze. To subscribe, go<br />

to www.pinpub.com/foxtalk/subscrib.htm or call<br />

1-800-788-1900.<br />

Developer Solutions–Online<br />

If you haven’t already done so, go to www.pinpub.com<br />

and click on Developer Solutions–Online to check out the<br />

FoxTalk index of articles for free. (Tell your colleagues!)<br />

You can browse the entire index or enter a specific search<br />

term to pinpoint all of the related articles. If you don’t<br />

already have the material you need on hand, you may<br />

purchase individual articles (including the Subscriber<br />

Download file, if applicable) for $5 each online immediately.<br />

Visual FoxPro tips are free! No subscription required.<br />

Help us compile valuable salary information for you by participating in Pinnacle Publishing’s anonymous, online salary survey.<br />

Simply go to http://www.pinpub.com/foxtalk/FTsalfrm.htm (you don’t need to be a subscriber) and<br />

take two or three minutes to complete the survey. Watch for the results soon!<br />

FoxTalk (ISSN 1042-6302) is published monthly (12 times per year)<br />

by Pinnacle Publishing, Inc., 1503 Johnson Ferry Road, Suite 100,<br />

Marietta, GA 30062. The subscription price of domestic<br />

subscriptions is: 12 issues, $179; 24 issues, $259. POSTMASTER: Send<br />

address changes to FoxTalk, PO Box 72255, Marietta, GA 30007-2255.<br />

Copyright © 1998 by Pinnacle Publishing, Inc. All rights reserved. No<br />

part of this periodical may be used or reproduced in any fashion<br />

whatsoever (except in the case of brief quotations embodied in<br />

critical articles and reviews) without the prior written consent of<br />

Pinnacle Publishing, Inc. Printed in the <strong>Un</strong>ited States of America.<br />

Brand and product names are trademarks or registered trademarks<br />

of their respective holders. Microsoft is a registered trademark of<br />

Microsoft Corporation. The Fox Head logo, FoxBASE+, FoxPro, and<br />

Visual FoxPro are registered trademarks of Microsoft Corporation.<br />

FoxTalk is an independent publication not affiliated with Microsoft<br />

Corporation. Microsoft Corporation is not responsible in any way for<br />

the editorial policy or other contents of the publication.<br />

This publication is intended as a general guide. It covers a highly<br />

technical and complex subject and should not be used for making<br />

decisions concerning specific products or applications. This<br />

Editor Whil Hentzen; Editorial Advisory Board Scott<br />

Malinowski, Walter Loughney, Les Pinter,<br />

Ken Levy; Publisher Robert Williford;<br />

Vice President/General Manager Connie Austin;<br />

Managing Editor Heidi Frost; Copy Editor Farion Grove<br />

Direct all editorial, advertising, or subscriptionrelated<br />

questions to Pinnacle Publishing, Inc.:<br />

1-800-788-1900 or 770-565-1763<br />

Fax: 770-565-8232<br />

Pinnacle Publishing, Inc.<br />

PO Box 72255<br />

Marietta, GA 30007-2255<br />

E-mail: foxtalk@pinpub.com<br />

Pinnacle Web Site: http://www.pinpub.com<br />

FoxPro technical support:<br />

Call Microsoft at 206-635-7191 (Windows)<br />

or 206-635-7192 (Macintosh)<br />

publication is sold as is, without warranty of any kind, either express<br />

or implied, respecting the contents of this publication, including<br />

but not limited to implied warranties for the publication,<br />

performance, quality, merchantability, or fitness for any particular<br />

purpose. Pinnacle Publishing, Inc., shall not be liable to the<br />

purchaser or any other person or entity with respect to any liability,<br />

loss, or damage caused or alleged to be caused directly or indirectly<br />

by this publication. Articles published in FoxTalk reflect the views of<br />

their authors; they may or may not reflect the view of Pinnacle<br />

Publishing, Inc. Inclusion of advertising inserts does not constitute<br />

an endorsement by Pinnacle Publishing, Inc. or FoxTalk.<br />

User ser name<br />

Passw assw asswor or ord<br />

yield<br />

zealot<br />

24 FoxTalk December 1998<br />

http://www.pinpub.com

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!