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