foxtalk - deutschsprachige foxpro user...

24
1 Un-Mapping Mapped Network Drives Andrew Coates 3 Editorial: Bug Fixes Available at microsoft.com Whil Hentzen 6 Best Practices: Seeing Patterns: The Mediator Jefferey A. Donnici 10 Reusable Tools: Mining for Gold in the FFC Doug Hennig 15 ActiveX and Automation Review: Integrating ADO and Visual Basic into Your VFP Applications, Part 2 John V. Petersen 19 The Kit Box: Second Star on the Right and Straight on ’til Morning Paul Maskens and Andy Kramek 23 December Subscriber Downloads EA Creating a Set of Business Rules and the Making of a BusinessRule Server Stephen Settimi EA To Open the Tables or Not to Open the Tables— That is the Question Jim Booth EA Follow-up: To PrintScreen or Not to PrintScreen Art Bergquist December 1998 Volume 10, Number 12 Fox Talk Continues on page 3 Solutions for Microsoft® FoxPro® and Visual FoxPro® Developers Accompanying files available online at http://www.pinpub.com/foxtalk Applies specifically to one of these platforms. Applies to FoxPro v2.x Applies to VFP v5.0 Applies to VFP v3.0 Applies to VFP v6.0 (Tahoe) 6.0 6.0 6.0 Un-Mapping Mapped Network Drives Andrew Coates One commonly stored piece of information is the location of a file or folder. If that file or folder is on the network, however, different users might refer to the location through different drive mappings. In this article Andrew shows how to decode the mapped location using the Windows API so that any user can view or retrieve the file or folder. I learned something this week that I guess I should have known. The Win32API isn’t consistent across operating systems. There are functions that work fine under Windows NT but fail miserably when called under Win95. I found one when working on the subject matter for this article. Mapping network drives is a common way of simplifying network storage systems from a user’s point of view. Unfortunately for the cause of universal access, however, different users map network shares to different driver letters. If your application allows users to store links to files, how can you tell whether E:\COMMON DOCUMENTS\BUDGET98.XLS and W:\BUDGET98.XLS refer to the same document? Even more importantly, how can a third user who has neither E: or W: mapped retrieve the document? The answer is UNC. The Universal Naming Convention uses the format \\SERVER\SHARENAME\ to refer to the location of a file or folder. In the preceding example, the first user might have mapped drive E to \\SERVER_PDC\PUB, and the second might have mapped drive W to \\SERVER_PDC\COMMONDOC. Any Visual FoxPro function that accepts a path as a parameter will handle UNC paths. However, returning a UNC path from the getfile() and getdir() functions is a completely different matter. These functions are central to allowing your users to specify the location of files and folders. The getfile() function will return a UNC path, but only if the user navigates to the file 6.0 6.0

Upload: nguyenanh

Post on 30-Jun-2018

230 views

Category:

Documents


1 download

TRANSCRIPT

1 Un-Mapping MappedNetwork DrivesAndrew Coates

3 Editorial: Bug Fixes Availableat microsoft.comWhil Hentzen

6 Best Practices: SeeingPatterns: The MediatorJefferey A. Donnici

10 Reusable Tools: Miningfor Gold in the FFCDoug Hennig

15 ActiveX and AutomationReview: Integrating ADOand Visual Basic into YourVFP Applications, Part 2John V. Petersen

19 The Kit Box: Second Staron the Right and Straighton ’til MorningPaul Maskens and Andy Kramek

23 DecemberSubscriber Downloads

EA Creating a Set of BusinessRules and the Making ofa BusinessRule ServerStephen Settimi

EA To Open the Tablesor Not to Open the Tables—That is the QuestionJim Booth

EA Follow-up: To PrintScreenor Not to PrintScreenArt Bergquist

December 1998Volume 10, Number 12

FoxTalk

Continues on page 3

Solutions for Microsoft® FoxPro® and Visual FoxPro® Developers

Accompanying files available onlineat http://www.pinpub.com/foxtalk

Applies specifically to one of these platforms.

Applies toFoxPro v2.x

Applies toVFP v5.0

Applies toVFP v3.0

Applies to VFPv6.0 (Tahoe)

6.06.06.0

Un-Mapping MappedNetwork DrivesAndrew Coates

One commonly stored piece of information is the location of a file or folder. If thatfile or folder is on the network, however, different users might refer to the locationthrough different drive mappings. In this article Andrew shows how to decode themapped location using the Windows API so that any user can view or retrieve thefile or folder.

I learned something this week that I guess I should have known. TheWin32API isn’t consistent across operating systems. There are functionsthat work fine under Windows NT but fail miserably when called under

Win95. I found one when working on the subject matter for this article.Mapping network drives is a common way of simplifying network storage

systems from a user’s point of view. Unfortunately for the cause of universalaccess, however, different users map network shares to different driver letters.If your application allows users to store links to files, how can you tell whetherE:\COMMON DOCUMENTS\BUDGET98.XLS and W:\BUDGET98.XLS referto the same document? Even more importantly, how can a third user who hasneither E: or W: mapped retrieve the document?

The answer is UNC. The Universal Naming Convention uses theformat \\SERVER\SHARENAME\ to refer to the location of a file or folder.In the preceding example, the first user might have mapped drive E to\\SERVER_PDC\PUB, and the second might have mapped drive W to\\SERVER_PDC\COMMONDOC.

Any Visual FoxPro function that accepts a path as a parameter will handleUNC paths. However, returning a UNC path from the getfile() and getdir()functions is a completely different matter. These functions are central toallowing your users to specify the location of files and folders. The getfile()function will return a UNC path, but only if the user navigates to the file

6.06.0

2 http://www.pinpub.comFoxTalk December 1998

http://www.pinpub.com 3FoxTalk December 1998

From the Editor FFFoooxxxTalk

Bug Fixes Availableat microsoft.comWhil Hentzen

IN the olden days of Fox Software, the gang would fixbugs and release patch disks. Some of you rememberthe 11 sets of patches for FoxPro 2.0, including two that

were released, ahem, four days apart. The patches wereshipped out for free (I think), but you had to know aboutthe patches and request them.

Distribution of bug fixes is a lot easier these days—nearly painless—because of the Web: Just go to the siteand download them. But you still have to know aboutthem. And if you don’t “know someone,” how are yougoing to find out? You could visit the Web site of themanufacturer of every product you use, but that’s notalways a great solution. And even if you had the time,sometimes it takes a lot of digging.

Visual Studio SP-1 (”SP” = Service Pack—softie speakfor bug fixes) is now available for free order anddownload from Microsoft. It addresses specific binarycompatibility issues with certain runtime files in VisualStudio 6.0—none of which I found applicable to VFP—butit’s probably still worthwhile getting.

A more important update—and one that’s fairlywell hidden—is the new Setup Wizard. If you’ve gottento the point of shipping a VFP 6 application, you’vepossibly run into one or more of the half-dozen defects inthe current Setup Wizard. A couple were pretty ugly, Ithink, but remember that FoxPro 2.5 shipped, and then

everyone found out that the Standard versiondidn’t work—period. Turns out everyone wastesting the Extended version with their brand-new386 machines. Similarly, testing the Setup Wizard ispretty tough, considering you need a functioningapp first.

What’s been fixed? Options for creating a DEP fileare now saved in WZSETUP.INI. Set Mark To “.”doesn’t blow out the distribution disks; neither doesinstalling multiple OCX files in the source directory.FOXHHELP.EXE is now copied correctly if you check theHTML Help Engine check box. And if you have two fileswith the same name but in different directories—like ifyou were shipping a couple of sets of tutorial data—eachis copied correctly now.

There’s also an updated VFP 6.0 Component Gallery,updates to several FoxPro Foundation Class (FFC) files,and an ActiveX control updater that will automaticallyupdate the Treeview, Listview, and Imagelist controls tothe latest versions.

Where do you get these new updates? Pop overto msdn.microsoft.com/vfoxpro, and select Samplesand Downloads, Product Updates, or, if you want thedirect URL, try http://msdn.microsoft.com/vfoxpro/downloads/updates.asp. Note that your copy must beregistered before you can get access to the page. ▲

Un-Mapping . . .Continued from page 1

through the Network Neighborhood—something a userwho’s used to having a network drive mapped is unlikelyto do. The getdir() function is even worse. There’s no wayof getting it to return a UNC path to a folder (except inone specific instance—see the sidebar “Returning a UNCPath from getdir()” for details). Not only do the functionsnot return UNC paths, but VFP doesn’t provide any wayto convert a mapped path to a UNC path.

Win32API to the rescue!The Windows 32-bit API provides a function that

accepts a mapped path and returns the UNC path that

corresponds to that mapped path. I searched throughthe API documentation and found a function calledWNetGetUniversalName() in MPR.DLL in the systemdirectory. The function will accept the path to either a fileor a folder with or without a trailing backslash.

Unfortunately, this function is only availableunder Windows NT. I found this out the hard way,having developed and tested a routine usingWNetGetUniversalName() on my WinNT developmentbox, I proudly installed it on a client’s Win95 machine,only to have it not work <very big sigh>. Microsoft has aKnowledge Base article confirming that it doesn’t workunder Win95 (there’s no mention in the article of Win98).So much for a single, consistent Win32API!

4 http://www.pinpub.comFoxTalk December 1998

What the article does mention, though, is a couple ofworkarounds. The first involves about 30 lines of C codethat calls another couple of functions in MPR.DLL. Thesecond mentions in passing that there’s yet anotherfunction in MPR.DLL called WNetGetConnection().WNetGetConnection() is not quite as versatile asWNetGetUniversalName(). It only accepts a drive letterand a colon (for instance, H:) rather than a full path.With a little string manipulation, however, it’s prettyeasy to split up a mapped path, pass the function what itwants, and then re-assemble a complete UNC path fromthe pieces.

To take the drudgery out of calling the Windows API,I wrote a wrapper function called GetUNCPath() that Iinclude in any project in which I call either getdir() orgetfile(). You could, of course, write wrappers for getfile()and getdir() that called GetUNCPath().

For example, if the pub share on the server_pdcserver were mapped as drive s:

? GetUNCPath('s:\documents')\\server_pdc\pub\documents? GetUNCPath('c:\temp')c:\temp? GetUNCPath('s:\documents\myfile.doc')\\server_pdc\pub\documents\myfile.doc

The wrapper function itself is pretty straightforward.The complete function is shown in Listing 1 and isavailable in this month’s Subscriber Downloads atwww.pinpub.com/foxtalk. It accepts the mapped drivepath as a compulsory parameter and optionally a lengthfor the UNC version of the path. This second parameter ismost often passed by the function itself in a recursive callif the default buffer size guess isn’t large enough (more onthis later).

Listing 1. The complete GetUNCPath source code.

* Program....: GetUNCPath.prg* Version....: 1.0* Author.....: Andrew Coates* Date.......: September 28, 1998* Notice.....: Copyright © 1998 Civil Solutions, All* Rights Reserved.* Compiler...: Visual FoxPro 05.00.00.0415 for Windows* Abstract...: Wrapper to the API call that converts a* mapped drive path to the UNC path* Changes....:* Originally used WNetGetUniversalName, but that* doesn't work under Win95 (see KB Q131416). Now uses* WNetGetConnection, which uses a string rather than a* structure so STRUCTURE_HEADER is now 0.

lParameters tcMappedPath, tnBufferSize

* from winnetwk.h#define UNIVERSAL_NAME_INFO_LEVEL 0x00000001#define REMOTE_NAME_INFO_LEVEL 0x00000002

* from winerror.h#define NO_ERROR 0#define ERROR_BAD_DEVICE 1200#define ERROR_CONNECTION_UNAVAIL 1201#define ERROR_EXTENDED_ERROR 1208#define ERROR_MORE_DATA 234#define ERROR_NOT_SUPPORTED 50#define ERROR_NO_NET_OR_BAD_PATH 1203

#define ERROR_NO_NETWORK 1222#define ERROR_NOT_CONNECTED 2250

* Local decision - paths aren't likely to be longer* than this - if they are, this function calls itself* recursively with the appropriate buffer size as the* second parameter.#DEFINE MAX_BUFFER_SIZE 500

* String length at the beginning of the structure* returned before the UNC path.* ACC changed to 0 on 9/10/98 - Now using* WNetGetConnection, which uses a string rather than a* struct.#DEFINE STRUCTURE_HEADER 0

local lcReturnValue

if type('tcMappedPath') = "C" and ; ! isnull(tcMappedPath)

* Split up the passed path to get just the drive. local lcDrive, lcPath * Just take the first two characters - we'll put it * all back together later. If the first two * characters aren't a valid drive, that's okay. The * error value returned from the function call will * handle it.

* Case statement ensures we don't get the "cannot * access beyond end of string" error. do case case len(tcMappedPath) > 2 lcDrive = left(tcMappedPath, 2) lcPath = substr(tcMappedPath, 3) case len(tcMappedPath) <= 2 lcDrive = tcMappedPath

Returning a UNC Pathfrom getdir()If you pass getdir() an initial folder in UNC format, then that

folder will be the initially selected folder, but it won’t appear

in the list of drives in the dialog box (see Figure 1). If that

folder or one of its subfolders is selected, then the string

returned will be in UNC. Be careful, though. If you navigate to

a mapped drive from the drive’s drop-down box, there’s no

way to get back to the UNC drive.

Figure 1. Passinggetdir() a UNCpath will initiallydisplay the UNCpath in the dialogbox, but theunmapped drivewon’t appear inthe drop-down list.

http://www.pinpub.com 5FoxTalk December 1998

lcPath = "" endcase

declare INTEGER WNetGetConnection IN WIN32API ; STRING @lpLocalPath, ; STRING @lpBuffer, ; INTEGER @lpBufferSize

* Set up some variables so the appropriate call can * be made. local lcLocalPath, lcBuffer, lnBufferSize, ; lnResult, lcStructureString

* Set to +1 to allow for the null terminator. lnBufferSize = ; iif(pcount() = 1 or ; type('tnBufferSize') # "N" or ; isnull(tnBufferSize), ; MAX_BUFFER_SIZE, ; tnBufferSize) + ; 1 lcLocalPath = lcDrive lcBuffer = space(lnBufferSize)

* Now call the dll function. lnResult = WNetGetConnection(@lcLocalPath, ; @lcBuffer, @lnBufferSize)

do case

* String translated sucessfully. case lnResult = NO_ERROR * Actually, this structure-stripping is no longer * required because WNetGetConnection() returns a * string rather than a struct. lcStructureString = alltrim(substr(lcBuffer, ; STRUCTURE_HEADER + 1)) lcReturnValue = left(lcStructureString, ; at(chr(0), lcStructureString) - 1) + lcPath

* The string pointed to by lpLocalPath is invalid. case lnResult = ERROR_BAD_DEVICE lcReturnValue = tcMappedPath

* There's no current connection to the remote * device, but there's a remembered (persistent) * connection to it. case lnResult = ERROR_CONNECTION_UNAVAIL lcReturnValue = tcMappedPath

* A network-specific error occurred. Use the * WNetGetLastError function to obtain a description * of the error. case lnResult = ERROR_EXTENDED_ERROR lcReturnValue = tcMappedPath

* The buffer pointed to by lpBuffer is too small. * The function sets the variable pointed to by * lpBufferSize to the required buffer size. case lnResult = ERROR_MORE_DATA lcReturnValue = getuncpath(tcMappedPath, ; lnBufferSize)

* None of the providers recognized this local name * as having a connection. However, the network is * not available for at least one provider to whom * the connection may belong. case lnResult = ERROR_NO_NET_OR_BAD_PATH lcReturnValue = tcMappedPath

* There's no network present. case lnResult = ERROR_NO_NETWORK lcReturnValue = tcMappedPath

* The device specified by lpLocalPath isn't * redirected. case lnResult = ERROR_NOT_CONNECTED lcReturnValue = tcMappedPath

otherwise lcReturnValue = tcMappedPath

endcase

else lcReturnValue = tcMappedPath

endif

return lcReturnValue

Constants from the corresponding API headerfiles are reproduced in the function rather than in astandalone header to make it more portable, and acouple of local constants are also #DEFINEd for claritylater in the code.

The mapped drive parameter is checked, and if it’snot a character expression, or if it’s .NULL., then thefunction simply returns whatever was passed to it. If thetests are passed, then the API function is DECLAREd.Note the use of the Win32API rather than the specificMPR.DLL.

Splitting up the mapped path parameter into itscomponent pieces is simply a matter of taking the firsttwo characters and calling them the mapped drive, andtaking the rest of the expression and calling them thepath. There’s no problem if that’s not a valid assumptionbecause the API function will return an error value andwe’ll take the appropriate action.

Next I call the API function and check the returnvalue. The two important values I’m looking for areNO_ERROR (my personal favorite <g>) andERROR_MORE_DATA. The NO_ERROR code meansjust that: The drive mapping was decoded successfully,and the result is in the lcBuffer variable.

The value placed in lcBuffer is a null-terminatedstring. This means that the string is terminated by a chr(0)that needs to be stripped before we use it in VFP. I justappend the path part of the original mapped pathparameter to the translated drive returned by the PAI calland voila—a UNC version of the mapped path.

The ERROR_MORE_DATA return value tells me thatI didn’t allocate enough space in the return buffer, and Ineed to call the function again. Fortunately, once I get thiserror, I no longer need to guess how long to make thebuffer. In addition to returning the error, the API functionsets lnBufferSize to the value required so I can callGetUNCPath() recursively with the original mappeddrive path and lnBufferSize.

Each of the other error states indicates that themapping couldn’t be decoded for some reason or other.I’ve decided to treat them all the same way: Simply returnthe mapped path passed to the function.

While VFP is a wonderful development environment,it does have some limitations. Fortunately, it providesenough access to the Windows API to be able to workaround most of them. ▲

12COASC2.ZIP at www.pinpub.com/foxtalk

Andrew Coates is an independent developer/data consultant living in

the Olympic City—Sydney, Australia. He specializes in PC database

applications, particularly integrating tools and visualizing spatial data.

[email protected].

6 http://www.pinpub.comFoxTalk December 1998

Best Practices FFFoooxxxTalk

Seeing Patterns: The MediatorJefferey A. Donnici 6.06.0

This month’s column continues the series that looks at somecommon design patterns and how they can be found andused within our Visual FoxPro applications. The patterndiscussed this month is the Mediator pattern, which allows asingle object to handle the interaction between a set of otherobjects. In doing so, the set of mediated objects is lesscoupled to one another and can be extended or variedindependently. To illustrate the Mediator pattern in action,two very different examples are given.

WITH the last Best Practices column, I began a newseries called “Seeing Patterns.” For those whomissed that column (for shame!), the intent

behind this series is to discuss the real-world use of somecommon design patterns, using VFP examples forillustration. If you aren’t already familiar with the conceptof object-oriented design patterns, I encourage you tolook through the last column for some references and adiscussion that provides an introduction to the ideasbehind design patterns. Specifically, you should makesure to get a copy of Design Patterns: Elements of ReusableObject-Oriented Software by E. Gamma, R. Helm, R.Johnson, and J. Vlissides (Addison-Wesley, ISBN 0-201-63361-2). This is the most popular of all the patterns-related books, and any developer working with an object-oriented language should have it.

With the introduction to design patterns out of theway, I can dive right into this month’s discussion of theMediator pattern. This column provides an introductionto the Mediator and, more importantly, some real-life VFPexamples. Hopefully, the VFP-centric approach to thisdiscussion will help you learn to “see” this pattern inyour own work, thereby making it easier to pull it fromyour “toolbox” when solving a design problem in thefuture. Remember, however, that object-oriented designpatterns represent another layer of abstraction above thedesign process. As such, the patterns themselves aren’tspecific to any one programming language or design tool.

Why the Mediator?The purpose of the Mediator pattern is to provide acentral point for interaction between a set of similarobjects. By having all interaction in a set of objects gothrough one portion of the component or subsystem, thedependencies and coupling between the objects in that setare reduced. As explained in Design Patterns, the intent of

the Mediator is to “define an object that encapsulates howa set of objects interact. Mediator promotes loose couplingby keeping objects from referring to each other explicitly,and it lets you vary their interaction accordingly.”

A good object-oriented design typically has a largenumber of classes in it, simply because behavior and dataare spread out among the classes to provide cohesion. Themore a system is comprised of a set of highly specialized“pieces,” all working together to provide largerfunctionality, the more those pieces can be reused. On theextreme opposite end of the spectrum, a few large classesthat perform several different functions apiece aren’ttypically reusable in a different application context.

The flip side of this design principle, however, is thatthe relationships between (or “the coupling of”) all thoseobjects brings with it a higher learning curve for thedesigner and programmer, as well as the potential forreduced reusability. If each object in a subsystem mustknow the programmatic interface for all the other objectsin the subsystem, then the overall reusability of everyobject suffers. So, while a large number of objects in asystem typically means that they’re inherently morecohesive, each new object in the system has the potentialto increase the number of dependencies exponentially.

The Mediator provides a solution to this problem byacting as a central point of contact for all those intra-system communications. Think of the Mediator as a“traffic cop” that makes sure everyone gets through theintersection like they should, without anybody having toget out of their car and talk to another driver. As long asall the “mediatees” (the cars and drivers) are beingcoordinated by the “mediator” (the traffic cop), there isn’tany need for the objects (or grumpy drivers) to interactwith one another (see Figure 1).

The benefits of this approach are numerous. Thedecoupling of the “mediatees,” called “colleagues” inDesign Patterns, has been mentioned already, but there areothers as well. For example, your class hierarchies can beless deep because the need to subclass is reduced whenusing a Mediator. The Mediator defines the interactionbetween the pieces of the system, so only the Mediatorneeds to be subclassed when the nature of therelationships changes. Each of the classes themselvesremains the same, with the “translation” between themoccurring within the Mediator.

Also, the Mediator simplifies the nature of the

http://www.pinpub.com 7FoxTalk December 1998

relationships between the objects being mediated.Without the Mediator in place, the communicationsbetween objects would likely take the form of many-to-many relationships. With the Mediator, thosecommunications become a series of one-to-manyrelationships, which are far easier to understand andmaintain over the long term.

It should be noted, however, that the Mediator doeshave one significant drawback. Remember that the goalof the Mediator is to simplify the interaction betweenobjects. Unfortunately, the Mediator component itselfoften becomes fairly complex internally. In DesignPatterns, the point is made that the “Mediator patterntrades complexity of interaction for complexity in theMediator. Because a Mediator encapsulates protocols,it can become more complex than any individualcolleague. This can make the Mediator itself a monoliththat’s hard to maintain.” While there isn’t any easy wayaround this potential problem, it’s best to know inadvance that the Mediator portion of the design mightrequire special development considerations. Carefullycommenting and documenting the relationships thathave been abstracted from the objects into the Mediatorwill go a long way toward lessening the maintenancecost of the Mediator itself.

Example 1—the Forms ManagerOne of the most common examples of the Mediatorpattern—and you might already have this in your ownapplications or framework—is a system-wide FormsManager. The purpose of the Forms Manager is to providea centralized point for communicating with, and between,the open windows in your application. If, for example,you need one form to get a value from another form, or ifyou want to close all open forms in the application, thenthe Forms Manager can handle those requirements. Thefollowing code is a skeleton example of a Forms Managerclass, including some ideas on the functionality andbehavior that it might contain.

DEFINE CLASS cstFormManager AS Custom

PROTECTED aForms[1,3]

PROCEDURE GetFormCount IF THIS.NoForms() RETURN 0 ELSE RETURN ALEN(THIS.aForms, 1) ENDIF ENDPROC

PROCEDURE NoForms RETURN (TYPE('THIS.aForms[1, 1]') == 'L') ENDPROC

PROCEDURE FindForm LPARAMETERS tuParam1, tcParamType *-- The purpose of this method would be to *-- allow the developer to pass an object *-- reference, a class name, a caption, or *-- even an object name as a parameter. *-- The second parameter would indicate the

*-- type of variable passed in the first *-- parameter. This information would be used *-- to find the matching form in the *-- collection. If one is found, the row for *-- it within the .aForms collection is *-- returned. Otherwise, 0 is returned. ENDPROC

PROCEDURE FormExists LPARAMETERS tuParm1, tcParamType *-- This method uses the FindForm method to *-- see whether the indicated form exists. *-- Instead of returning a row number, *-- however, it just returns a logical *-- indicating the existence of the form *-- being searched for. RETURN (THIS.FindForm(tuParm1,tcParamType)=0) ENDPROC

PROCEDURE AddForm *-- This code is provided for illustration *-- purposes only. In a production *-- environment, you would want to provide *-- more exception handling and perhaps other *-- "notification" options to indicate that *-- a new form has been created. LPARAMETERS toForm LOCAL lcObjClass,lcObjName,loObject,lnNewRow

*-- If we don't have an object reference, we *-- can't continue the method. Otherwise, *-- grab the properties we will *-- store in the collection. IF TYPE("toForm") # "O" RETURN ELSE loObject = toForm lcObjName = loObject.Name lcobjClass = loObject.Class ENDIF

*-- If there are no forms, we're putting the *-- passed form into the first position in *-- the array. IF THIS.NoForms() lnNewRow = 1 ELSE *-- Otherwise, add a row to the end of the *-- array to make room for the passed form. lnNewRow = ALEN(THIS.aForms, 1) + 1 DIMENSION THIS.aForms[lnNewRow,ALEN(THIS.aForms,2)] ENDIF

THIS.aForms[lnNewRow, 1] = lcObjName THIS.aForms[lnNewRow, 2] = lcObjClass

Figure 1. The Mediator pattern decouples the objects in a systemso that the relationships between them can be handled in acentralized fashion.

8 http://www.pinpub.comFoxTalk December 1998

THIS.aForms[lnNewRow, 3] = loObject RETURN ENDPROC

PROCEDURE RemoveForm LPARAMETERS tuParm1, tcParmType *-- This method would allow a single form to *-- be removed from the collection. The *-- first parameter could be an object *-- reference, an index position within the *-- collection, a form name, or even a caption. *-- When the form is removed, the array *-- containing the collection would be *-- adjusted. If the last form in the *-- collection is removed, the first position *-- in the collection array would be set to *-- .F. to indicate an empty collection. ENDPROC

PROCEDURE RemoveAllForms *-- This method would allow all forms to be *-- removed from the collection. The *-- GetFormCount method would be used *-- to determine the number of forms in the *-- collection, and this method would iterate *-- through them to call the *-- .RemoveForm method for each. ENDPROC

PROCEDURE GetFormRef LPARAMETERS tuParm1, tcParmType *-- This method returns a reference to the *-- form indicated by the passed parameters. *-- The parameter might be a class name, an *-- index position within the collection, *-- a caption, or some other form-specific *-- property. The FindForm method would be *-- used to find the matching form row *-- within the collection. If there's no *-- matching form found, NULL would be *-- returned. ENDPROCENDDEFINE

The idea behind this class definition is that the FormsManager would always be available to the open forms inthe application. This is often handled by making theForms Manager itself a member of the omnipresent“application object.” Because the Forms Manager isalways available, the forms in the application can addthemselves to it when instantiating and removethemselves from it during the destruction process. Forexample, the following code would be placed into a baseform class’s Init() event to automate the process of addinga new form to the collection:

oApp.oFormsManager.AddForm(THISFORM)

When the form is being closed, the following lineof code in the form’s Destroy() event would promptthe Forms Manager to remove the closing form fromthe collection:

oApp.oFormsManager.RemoveForm(THISFORM)

Obviously, the preceding example code isn’t acomplete Forms Manager solution, but it illustrates avariety of reasons that a Forms Manager’s functionalitywould be beneficial. Because you could get an objectreference to any specific form in the collection, you couldalways communicate with the precise form you need in

any situation. If that form isn’t available, the FormsManager can indicate this as well so that you canproceed accordingly.

Example 2—the dialog boxAnother problem that the Mediator pattern helps tosolve is the issue of communications between reusablecontainers that are conditionally appearing in yet anothercontainer. Most dialog boxes contain interface elementsthat are used throughout an application. For example, afile-selection dialog box contains a directory list box, butthe dialog box is hardly the only need for that control.Also, dialog boxes usually have controls that turn on/offor become enabled/disabled, based on the conditions andstate of other controls in the dialog box. For example, ifthe user absolutely must choose a file, then the “OK”button would be disabled until the user has selected a file.

Obviously, dealing with all these layers ofcommunication can become a problem, especially if thesame control/container appears in a variety of dialog box-style interfaces. Enter the Mediator pattern as an elegantsolution. By providing a single point of interface for allthe controls in the dialog box, the problem of changingthe state of other controls becomes centralized.

The dialog box shown in Figure 2 is a simple exampleof a window that contains three different containers. Eachof the containers provides a mechanism for setting a valueto on or off, while only two of them provide a mechanismfor displaying the current state of that value.

The key here is that the dialog box can have anynumber of similar containers added to it without a changeto anything else. That is, none of the other controls in thisdialog box would require changes, nor would the dialogbox itself, which is serving as the Mediator component inthis case.

Each of the containers in the window has twomethods that allow it to interact with the dialog box.These are UpdateMediator() and SetControls(), whichrespectively communicate with the mediator to reflect achange in state and receive messages from the mediator toupdate their member controls. For example, the containerwith the check boxes contains the followingUpdateMediator() code:

Figure 2. A simplified dialog box that allows for setting a form-wide value either on or off. While three of the containers can setthe value, only two display its current state.

http://www.pinpub.com 9FoxTalk December 1998

LPARAMETERS tlValue

IF PEMSTATUS(THIS.Parent, "UpdateMediator", 5) THIS.Parent.UpdateMediator(tlValue)ENDIF

Each of the check boxes calls this method in itsInteractiveChange() event, passing a logical parameteraccording to its role (“on” or “off”). The container thenverifies that its parent container has an UpdateMediator()method of its own before passing that value up to themediator. This allows the member container to be used inother types of Mediator-style designs, as well as indesigns that don’t require any sort of mediation approach.

The SetControls() method for each container receivesthe message from the dialog box and updates the controlswithin it. This lets the mediator communicate with each ofits “mediatees” without having to know the specificinternals (the type of control) of those containers. Againusing the check boxes container as an example, here’s theSetControls() method:

LPARAMETERS tlValue

*-- Note here that I've changed the*-- check boxes to usual boolean values*-- instead of numerics.THIS.chkOn.Value = (tlValue)THIS.chkOff.Value = (NOT tlValue)

With this functionality in place in each of thecontainers that might appear in the dialog box, we canthen look at the requirements of the mediator itself. Forstarters, the mediator needs to not only receive themessages from its “mediatees,” but it also needs tobroadcast interpretations of those messages down to them.For example, when the user chooses the “off” check box, amessage is sent up to the mediator to indicate that thestate is now “off.” A message then needs to be broadcastout to the member containers so that they can updatetheir own internal state to reflect this change.

Here are the contents of the dialog box’sUpdateMediator() method:

LPARAMETERS tlValue *-- This is the point at which the *-- mediator receives the message from *-- one of its member objects and sends *-- it to the SetControls method to be *-- broadcast down to all other member *-- objects. It's here that the one-to- *-- many relationship between the mediator *-- and its colleagues is leveraged. THISFORM.SetControls(tlValue)

Pretty simple, huh? The truth is that this example isstraightforward because nothing happens to the valuethat’s received before it’s transmitted back to the membercontainers. It’s a logical value in either case, so we cansimply pass it through to the SetControls() method of themediator/dialog box. Here’s the content of that method,which is only slightly more complex thanUpdateMediator():

LPARAMETERS tlValue

FOR EACH loMember IN THIS.Controls IF PEMSTATUS(loMember, "SetControls", 5) loMember.SetControls(tlValue) ENDIF ENDFOR && EACH loMember IN THIS.Controls

THISFORM.Refresh()

When the SetControls() method receives theparameter, it simply loops through its collection ofmember controls and broadcasts the value to each ofthem. Note that it must check for the existence of the“SetControls” method before doing the broadcast so thatcalling a non-existent method on the “Close” buttondoesn’t trigger an error.

“Seeing” the patternIt’s important to realize that the “mediator” doesn’talways have to be a single class definition, separate fromthe objects being mediated. Remember that the Mediatordescribes a pattern to a design solution, not an actualdesign. As such, I hope you don’t think of the twoexamples I’ve given here, both of which are in thismonth’s Subscriber Downloads at www.pinpub.com/foxtalk, as the only possible types of Mediator designsyou’ll see.

For example, you might have a single container classthat implements a Mediator pattern, using its ownmember controls. The preceding example uses differentclass definitions to illustrate the potential for reusabilitybetween the mediator and the components it mediates.However, suppose you have a form with a variety of textboxes and labels that work together as a type of calculator.When values are entered into the text boxes, the labelsmight be updated to indicate some calculation of thosevalues. In this case, you might not have a need forseparate container classes. Still, it would be unwieldy(and require far too much maintenance) for every text boxto know how to update the value for every label on theform. Instead, you might provide a single method thateach text box calls when it’s been updated. That methodcould perform the necessary calculations and update thelabel captions accordingly.

My point is to demonstrate that the Mediator pattern,and indeed all patterns, can exist at a variety of levels ofgranularity. While the Forms Manager example is asystem-wide, global implementation of the Mediatorpattern, the dialog box example is smaller in scope anddoesn’t have as wide an effect. A container like the“calculator” mentioned previously is even more of a“micro” approach to using the Mediator pattern.

In summaryHopefully, these VFP examples of the Mediator patterngot you thinking. I’m pretty certain that most of you willfind examples of the Mediator pattern that already exist in

Continues on page 14

10 http://www.pinpub.comFoxTalk December 1998

Reusable Tools FFFoooxxxTalk

Mining for Gold in the FFCDoug Hennig 6.06.0

Sometimes you have to sift through a lot of rocks beforeyou find true nuggets of gold. Not so with the FoxProFoundation Classes that ship with VFP 6; there’s tons ofgold in them thar hills.

ONE of the design goals for VFP 6 was to make iteasier for programmers new to VFP to get up andrunning with the tool. The Application Wizard is

one of the results of this goal, and the FoxPro FoundationClasses—or FFC—is another. The FFC, located in the FFCsubdirectory of the VFP home directory, is a collection ofclass libraries that provide a wide range of functions.Don’t think that just because previous versions of FoxProhave included some, shall we say, less useful (to be polite)example files that these fall into that category. While someof these do appear to be more demoware than reallyuseful, there are still lots of great classes in here. It’s wellworth the effort to spend some time looking at theseclasses, picking through the rocks to find the nuggets.

The best way to check out the FFC is using a new VFP6 tool called the Component Gallery, accessible from theVFP Tools menu. I won’t discuss the features of theComponent Gallery in this article; it’s simple enough touse that you can navigate your way around just byplaying with it. The FFC classes are displayed in theFoundation Classes folder of the Visual FoxPro Catalog(when I refer to where classes can be found later in thisarticle, I won’t specify this folder or this catalog, just thesubfolder under Foundation Classes). Classes are groupedby type (for example, Buttons, Dialogs, and Utilities) anddisplay a description in the status panel when you selectthem. Even better, right-clicking on a class displays acontext menu giving you access to the Help topic andsample files (either to run or to view) for that class. Thismakes it very easy to look through the FFC and see whichclasses might interest you.

This article will look at some of the FFC classes andsee how we might use them or even subclass them tomake them even more useful. Before we get started,though, I’ll discuss _BASE.VCX.

Base classesVFP 6 includes a set of subclasses of VFP base classes in_BASE.VCX. Although these classes aren’t located in theFoundation Classes folder in the Component Gallery(they’re in the My Base Classes folder), this VCX is

located in the FFC subdirectory, and all FFC classes aresubclassed from _BASE classes. If we use FFC classes inour applications, _BASE.VCX comes along for the ride.So, a question arises: Although this column has beendeveloping a robust set of base classes, and many of youhave your own set, should we consider using _BASEclasses for our base classes instead? The reason for evenconsidering this is, why have two VCXs in our projectsthat provide essentially the same thing?

After looking at the _BASE classes, the conclusionI’ve come to is no; I’ll continue using my ownSFCTRLS.VCX classes. _BASE classes don’t have thevisual changes I’ve made to my base classes—forexample, the AutoSize property for classes such as_CheckBox and _Label is set at the default .F.; I almostalways want it set to .T., so that’s what I’ve done inSFCheckBox, SFLabel, and other classes with thisproperty. Furthermore, _BASE classes also don’t havethe behavior I want—for example, their Error methodspass the error on to an ON ERROR handler rather thanusing the Chain of Responsibility design pattern (up theclass hierarchy and then the containership hierarchy)that my classes do, as I discussed in my January 1998column (see “Error Handling Revisited”). Finally, theseclasses have a lot of custom properties and methods thatsupport the Application Wizard. Since I don’t plan touse the Application Wizard to create applications, theseproperties and methods would just complicate things. So,we’ll just have to live with the fact that _BASE.VCX willbe included in our projects, whether we want it or not, ifwe use FFC classes.

_SysToolbarsIt’s highly unlikely you’ll want the VFP developmentenvironment toolbars (such as the Standard and Databasetoolbars) to be visible when an application is running.Unfortunately, there isn’t a single command that hidesthem all, so most developers create an array of toolbarnames, spin through the array and see whether thecurrent toolbar is visible, and, if it is, hide it. Of course,you have to do just the opposite when the applicationcloses down, at least in a development environment.

Because this is a common task, the FFC includes aclass called _SysToolbars; this class is located in_APP.VCX and appears as “System Toolbars” in theApplication subfolder of the Component Gallery. This

http://www.pinpub.com 11FoxTalk December 1998

class is very simple; it hides and restores the systemtoolbars either automatically or manually. To use itautomatically, either pass .T. to its Init method when youinstantiate it programmatically or set its lAutomaticproperty to .T. in the Property Sheet when you drop it ona form. In this case, any visible system toolbars are hiddenwhen the object is instantiated and redisplayed when it’sdestroyed. To use it manually, call its HideSystemToolbarsand ShowSystemToolbars methods. You’ll likely want toinstantiate it as soon as possible at application startupusing code similar to this:

oSysToolbars = newobject('_SysToolbars', '_app.vcx', ; '', .T.)

As long as oSysToolbars stays in scope, the toolbarsstay hidden. You can either manually release this object orlet it go out of scope when the application terminates.

_ResizableThis class, which is defined in _CONTROLS.VCX andappears as “Resize Object” in the User Controls subfolderof the Component Gallery, moves and resizes all thecontrols in a form when the form is resized. It takes careof all the mundane details of drilling down throughcontainers (such as PageFrames), adjusting the Top, Left,Height, and Width properties of controls. I’ve done thiskind of thing manually before in the Resize method of theform, writing reams of code to handle all the applicablecontrols. Believe me, anything that can automate thistedious chore is very welcome.

To see how _Resizable works, run the ResizeDemoform (by the way, this form also contains an _SysToolbarsobject with lAutomatic set to .T. so you can see how thatclass works). Leave “Resize” unchecked and resize theform. See how dumb it looks as more background appearswhen you make the form larger or controls get hidden asyou make the form smaller? Not the sign of a professionalapplication. Now check “Resize” (but leave “SFResizable”unchecked for now); when you resize the form, the editbox is resized, and the other controls are movedautomatically.

However, there are a couple of problems with thisclass. First, it resizes or moves all the controls. Typically,when you resize a form, you want to resize certaincontrols (such as edit boxes) and move the ones belowand to the right of them to account for the new size; therest you want to leave alone. Another problem is that itmoves everything proportional to the original size of theform; the effect is that a resized form looks like a blownup or shrunk version of the original, with controls furtherapart or closer together. The result is sort of like whathappens to writing on a balloon as it’s blown up. In myexperience, what you really want is a form that looks justlike it was but with some of the controls larger or smallerand the rest just moved relative to the resized controls.

After all, the whole reason for the user to resize a form isto be able to see more of those controls they’d normallyhave to scroll (grids, list boxes, edit boxes, TreeView andListView controls, and so forth), not to get a bigger formwith more background.

The good news is that we don’t need to throw_Resizable out and create a new class with the behaviorwe want. _Resizable has all of the logic we need; it justdoesn’t do things quite right. So, let’s subclass it andmake the subclass act the way we expect.

The subclass I created is called SFResizable, and it’scontained in SFFFC.VCX in the Subscriber Downloads atwww.pinpub.com/foxtalk. I changed both the Height andWidth properties to 17 so the control doesn’t take up asmuch space when it’s dropped on a form. Since we needto treat some controls differently than others (some will bemoved, while others might be resized), we need a way toindicate how each control should be treated. I originallyconsidered adding new properties to my base classes (forexample, lResize, which, if .T., would indicate that thiscontrol should be resized) but rejected that because thenSFResizable would only work with a certain set of baseclasses, and that would make it far less reusable. Instead, Idecided to make SFResizable self-contained, so I createdthe following new properties:

• cRepositionLeftList: A comma-delimited list of thenames of controls that should be moved left-rightonly as the form is resized.

• cRepositionList: A comma-delimited list of the namesof controls that should be moved left-right and up-down as the form is resized.

• cRepositionTopList: A comma-delimited list of thenames of controls that should be moved up-downonly as the form is resized.

• cResizeList: A comma-delimited list of the names ofcontrols that should be resized as the form is resized.

I also overrode the AddToArray and SetSizemethods. AddToArray adds size and position informationabout a control to an array property of the class; it’scalled from the LoopThroughControls method, whichprocesses all of the controls in the form. The problemwith the original AddToArray is that it stores the size andposition information of the control as values proportionalto the size and position of the form rather than the actualvalues for the control. So, I simply used the Visual Basicmethod of subclassing (I copied the code from_Resizable.AddToArray and pasted it into SFResizable’smethod) and then modified the code to store the originalvalues. SetSize is also called from LoopThroughControls;it adjusts the size and position of a control by thedifference between the original (stored in the

12 http://www.pinpub.comFoxTalk December 1998

InitialFormHeight and InitialFormWidth properties) andcurrent sizes of the form. Since I don’t want every controlresized and moved, I changed the code to use thefollowing logic:

• If the control’s name is in the cRepositionListor cRepositionTopList properties, its Top valueis adjusted.

• If the control’s name is in the cRepositionListor cRepositionLeftList properties, its Left valueis adjusted.

• If the control’s name is in the cResizeList property,its Width and, for certain types of controls (Label,Editbox, Listbox, Grid, PageFrame, Line, OLEControl,OLEBoundControl, Shape, and Container), Heightvalues are adjusted.

To use SFResizable, drop it on a form and call itsAdjustControls method in the Resize method of the form.Enter the names of those controls that should be resizedas the form is resized into the cResizeList property of theSFResizable object; grids, edit boxes, and other controlsthat can scroll are obvious candidates. Enter the names ofthose controls that should be moved up-down and left-right as the form is resized into the cRepositionListproperty. For example, controls below and to the right ofan edit box might qualify for this adjustment. For thosethat should only be moved up and down, enter theirnames into the cRepositionTop property. Examplesinclude controls below a grid, because these controls needto move up or down as the grid’s Height is changed.Finally, enter the names of controls that should be movedleft and right, such as those to the right of an edit box, asthe form is resized into the cRepositionLeft property.

Run the ResizeDemo form again, but this time checkboth “Resize” and “SFResizable”. When you resize theform, the text box beside “Label 1” doesn’t move; the editbox and shape surrounding it don’t move but are resized;the “Resize” and “SFResizable” check boxes and the checkboxes beside the edit box move left and right but not upand down; the text box beside “Label 2” moves up anddown but not left and right; and the “Reset” buttonmoves both up-down and left-right. In other words, theform behaves as we’d expect when it’s resized.

A perfect addition to SFResizable would be a builderto make it easy to specify which controls should be movedor resized, with perhaps a list of the controls in the formand check boxes indicating how the selected controlshould be treated.

RegistryAs you know, the Registry is the “in” place to storeconfiguration, preference, and other application settings;INI files are officially passe (although I know lots of

developers who, like me, would rather lead a userthrough editing an INI file over the phone than darehave them use a tool like REGEDIT). Settings shouldnormally be stored in keys with a path similar toHKEY_CURRENT_USER\Software\My CompanyName\My Application\Version 1.1\Options.

The FFC Registry class (defined in REGISTRY.VCXand appearing as “Registry Access” in the Utilitiessubfolder of the Component Gallery) is a wrapper classfor the Windows API calls that deal with the Registry.Rather than having to know that you must open a keyusing the RegOpenKey function before you can useRegQueryValueEx to read the key’s value, all you haveto know with the Registry class is that you call itsGetRegKey method. Here’s an example that gets thename of the VFP resource file:

#include REGISTRY.H && located in HOME() + 'FFC'loRegistry = newobject('Registry', 'Registry.vcx')lcResource = ''loRegistry.GetRegKey('ResourceTo', @lcResource, ; 'Software\Microsoft\VisualFoxPro\6.0\Options', ; HKEY_CURRENT_USER)

(Yeah, I know it’s easier to use SET(‘RESOURCE’, 1), butthis is just an example.)

REGISTRY.VCX also contains subclasses of Registrythat make it easier to read and write VFP settings(FoxReg), ODBC settings (ODBCReg), applications byfile extension (FileReg), and even (horrors!) INI files(OldINIReg). Registry-specific constants are defined inREGISTRY.H so you can specify HKEY_CURRENT_USER(as I did in the preceding code) rather than its value(-2147483647).

Here are the useful methods in REGISTRY.VCX.Unless otherwise specified, all return a code indicatingsuccess (0, or the constant ERROR_SUCCESS) or theWinAPI error code; see REGISTRY.H for error codes.

• GetRegKey: Puts the value of the specified keyinto a variable.

• SetRegKey: Sets the value of the specified key to thespecified value (the entire key path is created if itdoesn’t exist).

• DeleteKey: Deletes the specified key (and all subkeys)from the Registry.

• DeleteKeyValue: Removes the value from thespecified key.

• EnumOptions: Populates an array with all thesettings under a specific key and their current values.

• IsKey: Returns .T. if the specified key exists.

Registry can be instantiated at application startup(perhaps by an Application object) to get the settings the

http://www.pinpub.com 13FoxTalk December 1998

application needs: location of the data files on a LAN,names of the most recently used forms (so they canappear at the bottom of the File menu like Microsoftapplications do), and so forth. It can also be dropped on aform to save and restore form-specific settings such as theLeft, Top, Height, and Width values (so it has the samesize and position as when this user closed it), grid columnwidths and positions (so users can rearrange grids andhave them appear that way the next time), and so on.

Being a picky guy, I have two problems with theRegistry class. First, because the return value ofGetRegKey is a success or error code, you have to pass thevariable you want the value placed into by reference. I’dprefer it to return the value of the key; after all, if an erroroccurs, the WinAPI error code is probably as useful to meas the “details” section of a GPF dialog, and even if I dowant it, it could easily be stored in a property of RegistryI can query. Second, it’s likely that both the key path(“Software\Microsoft\VisualFoxPro\6.0\Options” inthe previous example) and the user key (usuallyHKEY_CURRENT_USER) are going to be the same forevery method call of a specific instance of a Registryobject. Being the lazy sort, I’d rather put these values intoproperties and not pass them every time I call a method.

As with _Resizable, I’ve subclassed Registry intoSFRegistry (also in SFFFC.VCX) to have the behavior Iwant. Although I planned to, it turned out that I didn’thave to add properties for the default key path(cAppKeyPath) and user key (nUserKey)—they alreadyexisted, even though they’re not used by Registry(they’re used by the subclasses in REGISTRY.VCX). SinceRegistry.Init sets nUserKey to HKEY_CURRENT_USER,there normally isn’t even a need to change this property.I changed the GetRegKey, SetRegKey, DeleteKey,DeleteKeyValue, EnumOptions, and IsKey methods toaccept different parameters than the matching Registryclass methods (I rearranged the parameters so optionalones are at the end) and return a more appropriate value(the key value in the case of GetRegKey, the number ofoptions in the array in the case of EnumOptions, and .T.if the method succeeded in the case of SetRegKey,DeleteKey, and DeleteKeyValue). These methods alsostore the WinAPI result code into a new nResult property,which can be used to determine what went wrong if amethod fails. Here’s an example; this is the code fromEnumOptions:

lparameters taRegOptions, ; tlEnumKeys, ; tcKeyPath, ; tnUserKeylocal lcKeyPath, ; lnUserKey, ; lnReturn

* If the key path and user key weren't passed, use the* defaults.

lcKeyPath = This.GetKeyPath(tcKeyPath)lnUserKey = This.GetUserKey(tnUserKey)

* Use the parent class method to enumerate the key,* store the result code, and return the number of* options it found.

lnSuccess = dodefault(@taRegOptions, lcKeyPath, ; lnUserKey, tlEnumKeys)This.nResult = lnSuccesslnReturn = iif(lnSuccess = ERROR_SUCCESS, ; alen(taRegOptions, 1), 0)return lnReturn

GetKeyPath and GetUserKey are new methodscalled by all the overridden methods to either use thekey path and user key values passed, or the defaults(stored in the cAppPathKey and nUserKey properties)if they weren’t passed.

Here’s an example of the use of SFRegistry; this codewould be used in the Init method of a form to restore thesize and position it had the last time the user had it open.This code assumes that an SFRegistry object namedoRegistry was dropped on the form and its cAppPathKeyproperty was set in the Property Sheet to the key path forthis application.

lcKey = This.oRegistry.cAppPathKey + '\' + This.NamelcTop = This.oRegistry.GetRegKey('Top', lcKey)This.Top = iif(isnull(lcTop), This.Top, val(lcTop))* similar code for Left, Width, and Height

(Since the Registry class only supports reading andwriting strings, VAL() must be used on the returnvalue. Also, if this is the first time this form is run, akey might not exist for it in the Registry, in which case.NULL. is returned, so this code handles that case.)A method of the form (such as Release) would useThis.oRegistry.SetRegKey to save the current form sizeand position in the Registry.

For another example, run REGISTRYDEMO.PRG. Itcreates some new keys, displays their values, and showshow EnumOptions works. (It also deletes the keys itcreates so it doesn’t pollute your Registry permanently.)

_ObjectStateIf you’ve been doing your homework on design patterns,you’re probably aware of a pattern known as Memento.A Memento is intended to save the state of something,presumably so it can be restored after it’s been changed.The FFC includes a class called _ObjectState that’s sortof like a Memento: It saves the value of one or moreproperties of another object and can later restore them totheir former values.

_ObjectState is defined in _APP.VCX and appears as“Object State” in the Application subfolder in theComponent Gallery. It has an oObject property thatcontains an object reference to the object whose state itmaintains; this property can be set by passing the objectreference to the Init method of the _ObjectState object orby setting it manually. It has a Set method that sets thevalue of the specified property of the managed object tothe specified value, optionally first saving the original

14 http://www.pinpub.comFoxTalk December 1998

value in an array property of itself. It also has a Restoremethod that restores the value of one or all savedproperties. The Restore method is also called from theDestroy method, so when the _ObjectState object goes outof scope, everything is restored automatically. Since thearray property has a single row for each saved property,you can’t use this class to provide multiple levels of undo;however, you could subclass _ObjectState and add thisbehavior if desired.

Where might we use such a class? One situationinvolves things a user can change but might want tochange back. For example, you might allow a user torearrange and resize the columns in a grid, but provide a“Reset to Default” function that puts them back. Anotherplace would be when you temporarily change theproperties of an object (perhaps so it behaves differently),do something with the object, and then change them backagain. Rather than having a set of local variables that savethe property values and then having to manually restorethem before the routine ends, you could do somethinglike this:

local loObjectStateloObjectState = newobject('_ObjectState', '_app.vcx', ; '', This)loObjectState.Set('<first property', <new value>)loObjectState.Set('<second property', <new value>)* do something here

When this code ends, loObjectState goes out of scope,so the saved property values are automatically restored.

As usual, I have one slight quibble with the classimplementation: Saving the current value of the propertyis optional. There really isn’t a need to use _ObjectState ifyou’re not going to save the value (after all, you can juststore the new value to the property yourself in that case),so I think the default behavior should be to save andhave it optionally not save. So, I created a subclass of_ObjectState called SFObjectState (in SFFFC.VCX) thatsimply overrides the Set method, as follows:

lparameters tcProperty, ; tuValue, ; tlNoSavereturn dodefault(tcProperty, tuValue, not tlNoSave)

In other words, rather than passing .T. to save, you have

to pass .T. to not save.To see an example of this class in action, run

the ResizeDemo form, check both “Resize” and“SFResizable”, then resize the form. Now click on theReset button and watch everything snap back to theoriginal size and position. This was done by savingthe Height and Width properties of the form in itsInit method:

with This.oObjectState .oObject = This .Set('Height', This.Height) .Set('Width', This.Width)endwith

and restoring them in the Click method of the button:

Thisform.oObjectState.Restore()

Of course, we didn’t have to save the original sizeand position of every object on the form because changingthe form Height and Width causes the Resize method tofire, which moves everything for us. Also, although Ididn’t specify individual properties to Restore (so itrestored all saved values), I could have restored themindividually if I wanted that control.

ConclusionThe FFC contains a lot of useful classes. Some of themare great as is, while others can be subclassed to addminor improvements. Either way, I suggest you spendsome time looking at these classes and thinking abouthow you might use them. I’m sure you’ll find somegems in the pile. ▲

12DHENSC.ZIP at www.pinpub.com/foxtalk

Doug Hennig is a partner with Stonefield Systems Group Inc. in Regina,

Saskatchewan, Canada. He is the author of Stonefield’s add-on tools for

FoxPro developers, including Stonefield Database Toolkit and Stonefield

Query. He is also the author of The Visual FoxPro Data Dictionary in

Pinnacle Publishing’s The Pros Talk Visual FoxPro series. Doug has

spoken at the 1997 and 1998 Microsoft FoxPro Developers Conferences

(DevCon), as well as user groups and regional conferences all over

North America. He is a Microsoft Most Valuable Professional (MVP).

[email protected], [email protected].

your own code. That’s a good thing, because it means thatthe pattern is one you’re already recognizing. With anyluck, this discussion has given you some ideas for otherplaces in your work that a Mediator pattern mightincrease reusability, decrease coupling, and/or makecertain functionality possible.

Next month will bring another column in this “SeeingPatterns” series, with a new pattern being discussed.

Until then, please feel free to let me know if you have anyideas or suggestions for the Best Practices column. ▲

12DONNSC.ZIP at www.pinpub.com/foxtalk

Jefferey A. Donnici is the senior Internet developer at Resource

Data International, Inc. in Boulder, CO. He’s served as both a Mediator

and a Colleague on more than one occasion. Jeff is a Microsoft

Certified Professional and a four-time Microsoft Developer Most

Valuable Professional. 303-444-7788, fax 303-928-6605,

[email protected], [email protected].

Best Practices: Patterns . . .Continued from page 9

http://www.pinpub.com 15FoxTalk December 1998

ActiveX and Automation Review FFFoooxxxTalk

Integrating ADO andVisual Basic into Your VFPApplications, Part 2John V. Petersen 6.06.0

In Part 1 of this series in the October issue, John introducedyou to some basic (no pun intended <g>) techniques withregard to integrating ADO and Visual Basic into your VisualFoxPro applications. ADO offers unprecedented ways in whichto both access and move data on both an intra- and inter-application basis. Visual Basic, with its ability to create ActiveXcontrols, provides an indispensable mechanism for packingvisual application components that can be reused in anyapplication—whether it’s written in VFP, VB, or any otherenvironment capable of hosting ActiveX controls. This month,John concludes this discussion by expanding and refining thedesign started in October.

IN Part 1 of this series in the October issue, I showedyou how to build a Visual Basic ActiveX control thatencapsulated two things:

• The Microsoft OLE-DB DataGrid, version 6.0• The Microsoft ActiveX Data Objects Library,

version 2.0

The DataGrid itself is an ActiveX control. ADO is aset of COM objects used to access and update data, whichcan be stored in a variety of formats—either relational ornon-relational. For more details on what makes up ADO,please refer to Part 1 of this series.

The functionality of the ActiveX control was verysimple. ADO RecordSet and Connection objects werecreated, and the DataGrid’s DataSource property wasassigned the reference of the newly created RecordSetobject. This allowed the contents of the RecordSet objectto be displayed in the grid.

Why is it necessary to create what’s essentially anActiveX control of an ActiveX control? Visual Basicpossesses a couple of important capabilities that VFPdoesn’t currently support natively:

• ActiveX data binding• The ability to surface COM events

Currently, inside of VFP, an ADO RecordSet can’t bebound to the DataSource property of a DataGrid ActiveXcontrol. Further, when ADO objects such as the

Connection and RecordSet are created in VFP, theproperties and methods of those interfaces are exposed.However, VFP doesn’t have the ability to respond toevents raised by the Connection, RecordSet, or any COMobject created through the CreateObject function. Theonly way to have events surfaced in VFP is to create anActiveX control.

Limitations with the current solutionSeveral limitations exist with the current solution:

• Too much functionality—The functionality of creatingthe ADO objects and the presentation services offeredby the DataGrid are bundled together. Ideally, thesetwo items should be split into separate controls toprovide the most functionality. There will be timeswhen the services of ADO will be required—withoutthe need to display data in a grid.

• No public interfaces exist to make the data sourceconfigurable—The control is currently hard-wired toonly fetch data from the authors table of the SQLServer Pubs database.

• Surfaced events are private to the control—These eventsneed to be surfaced at the ActiveX control interface sothe host application—VFP, in this case—can recognizethe occurrence of these events.

What you need to work through the sample codeHere’s what you need to work through these examples:

• ADO 2.0• OLE-DB Provider for SQL Server (ships with

ADO 2.0)• OLE-DB Grid ActiveX control, version 6.0• SQL Server 6.5 or 7.0• Visual Basic 5.0 SP 3 or Visual Basic 6.0• Visual FoxPro 6.0

ADO 2.0 ships with Visual Studio 6. You canalso obtain ADO via a download from http://www.microsoft.com/data.

16 http://www.pinpub.comFoxTalk December 1998

Improving the interfaceFigure 1 illustrates the new Visual Basic project.

The first order of business is addressing the firstlimitation of too much functionality (yes, I know thatsounds funny). The following code is associated with thegrid control. Most of the code deals with the visualcharacteristics of the grid—location and size. The mostimportant method is the SetDataSource Member.Remember, in VFP, an ADO RecordSet can’t be bound tothe DataGrid control. This is how you can get around thatlimitation. Pass the ADO RecordSet to the control, andhave the control do the binding for you within itsboundaries! It works like a charm.

Private Sub UserControl_Initialize() 'When the control is dropped onto the form, 'the grid must be positioned in the upper- 'left corner of the container. With DataGrid .Left = 0 .Top = 0 End WithEnd Sub

Private Sub UserControl_Resize() 'When the container is resized, we want the 'grid to also resize. With DataGrid .Width = ScaleWidth .Height = ScaleHeight End WithEnd Sub

'WARNING! DO NOT REMOVE OR MODIFY'THE FOLLOWING COMMENTED LINES!'MemberInfo=14Public Sub SetDataSource(recordset As ADODB.recordset) Set DataGrid.DataSource = recordsetEnd Sub

With this design, the services of our grid controlare very granular. Its only purpose is to accept anADO RecordSet and display the data. We’ll reserve themore complicated tasks of responding to events foranother control.

The other control in the Visual Basic project consistsof one user interface element—a Label control. There’squite a lot of code behind this control—far too much to

print in its entirety. However, I’ll highlight the mostimportant pieces of code.

Going back to the limitations of the first design, nopublic interfaces existed to make the ADO DataSourcevariable. Also, all of the events in the previous designwere private. This meant that while the control within itsboundaries recognized the event, no mechanism existedfor the control to surface those events to its own interface.This means that any application environment hosting thecontrol wouldn’t have the ability to have its own codeexecute when those events have fired. After all, that’swhat it’s all about—having the ability to attach VFP codeto ADO events.

The first lines of code in the VB ActiveX controlare as follows:

Dim WithEvents oconn As ADODB.ConnectionDim WithEvents ors As ADODB.recordset

This provides that ability to the ActiveX control torespond to events that each of these objects will fire. Theprevious design took advantage of this ability andattached Visual Basic code to those events. In the olddesign, when an ADO connection was established, aMessageBox dialog box appeared, stating that the ADOconnection was complete. While not very useful, it servedto illustrate how events could be trapped. Visual Basiccode is nice, but we work in VFP. We need the ability tosurface these events so that VFP code can be used.

Custom eventsRemember when VFP 3.0 was released back in 1995? Wewere given the ability to define our own methods andproperties. Defining events, on the other hand, wassomething that wasn’t—and still isn’t—-provided. I, forone, didn’t see the big deal in this. After all, there arehundreds of events already defined. Why would I needmore? As you’ll see, the ability to define custom events isa very powerful capability.

To illustrate the power of events, I’ll focus on theMoveComplete event of the ADO RecordSet object. Thisevent fires whenever the record changes. This event getsfired as a result of invoking the MoveFirst, MovePrevious,MoveNext, or MoveLast methods. The MoveCompleteevent also fires when an ADO RecordSet is first opened.

Before looking at the custom event, let’s take a look atthe code for the internal event that gets fired:

Private Sub ors_MoveComplete( _ ByVal adReason As ADODB.EventReasonEnum, _ ByVal pError As ADODB.Error, _ adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.recordset) RaiseEvent movecomplete(adReason, pError, _ adStatusCancel, pRecordset)End Sub

In this code, the internal ors_RecordSet objectvariable will respond to the MoveComplete event. Thiscode block, in turn, will fire. The code to key on is theRaiseEvent statement. Notice that another MoveComplete

Figure 1. The new Visual Basic ActiveX control project containstwo ActiveX controls.

http://www.pinpub.com 17FoxTalk December 1998

event is being fired, and the arguments passed to theinternal event are being passed as well. What’s goingon here?

In this new design, a public event has been createdfor each of the internal events. To keep things consistent,the same name was used. The following line of code in theVisual Basic control defines the event:

Event MoveComplete( _ ByVal adReason As ADODB.EventReasonEnum, _ ByVal pError As ADODB.Error, _ adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.recordset)

There are 11 events for the ADO RecordSet object. Thenew ActiveX control has 11 custom events that correspondto each to each of the RecordSet events that will get firedinternally. In addition to surfacing events, three publicproperties were created as well. These properties are:

• Provider—This specifies the name of the OLE-DBprovider to use.

• ConnectionString—This specifies the connectionstring used to create an active ADO connection.

• RecordSetSource—This specifies the source of thedata contained in an ADO RecordSet. This sourcecould be the name of a table or a SQL statement.

Finally, two public methods exist that do the work ofcreating both the ADO connection and RecordSet objects.These methods are:

• CreateConnection—This method uses the Providerand ConnectionString properties to establish anADO connection.

• CreateRecordSet—This method uses both theConnection object created by the CreateConnection

method and the RecordSetSource property toestablish the ADO RecordSet object.

With a new design, it’s time to host the controlsin VFP.

Hosting the controls in VFPBefore you can use the controls, they must be registered inVFP. Figure 2 shows how the entries will appear in theTool\Options\Controls dialog box.

Figure 3 illustrates the VFP form used to host thesenew controls.

The following code in the Init event of the forminitializes the controls to both get and display data:

Local connstringconnstring = "Persist Security Info=False;"connstring = connstring ; + "User ID=sa;Initial Catalog=pubs;"connstring = connstring + "Data Source=(local)"

With This.vbADO .Provider = "SQLOLEDB.1" .ConnectionString = connstring .RecordSetSource = "authors" .CreateConnection .CreateRecordsetEndWith

With This .ors = This.vbADO.adorecordset .oconn = This.vbADO.adoconnectionEndWith

This.vbgrid.SetDataSource(This.ors)

Figure 4 illustrates the VFP form in action.So then, where do the events fit in? The following

code is contained in the MoveComplete event of thevbADO ActiveX control:

*** ActiveX Control Event ***LPARAMETERS adreason, perror, adstatus, precordsetThisform.tmrRefresh.Enabled = .T.

Whenever the MoveComplete event fires,the tmrRefresh object on the form is enabled. Thefollowing code is contained in the Timer event of the

Figure 2. The new Visual Basic ActiveX controls must beregistered for use in VFP.

Figure 3. The new Visual Basic ActiveX controls hosted bya VFP form.

18 http://www.pinpub.comFoxTalk December 1998

tmrRefresh object:

ThisForm.RefreshThis.Enabled = .F.

To help make the point clearer, the following is thecode for the cmdNext CommandButton:

ThisForm.ors.MoveNext

Did you notice that a call to the Refresh method of theform isn’t present in the CommandButton? The same istrue for the other navigation buttons as well. What doesthis mean? It means that code only has to exist in onelocation. The alternative is to have method calls inmultiple locations. Since the COM events aren’t surfacedin VFP, this is usually a required course of action. With theability to both recognize events and attach VFP code tothose events, refresh code only has to be written onceand, most importantly, only called from one location: theMoveComplete event. What’s the benefit? Whether younavigate via a CommandButton or the DataGrid control,the MoveComplete event will fire. The form will refresh.Clearly, this is a much better design—especially from acode maintenance standpoint.

At this point, you might be asking, why the TimerControl? I had problems calling the Refresh methoddirectly in the event. To avoid errors and get the correctfunctionality, the Refresh method had to be called afterthe MoveComplete event fired. At this point, I don’tknow what’s causing the problem. I’ll be reporting thisto the VFP team at Microsoft and will report back ontheir response.

A quick word on the _VFP AutoYield propertyBy default, the AutoYield property of the _VFP object isset to .T. This means that VFP will automatically processpending Windows events that occur between theexecution of VFP program code. For this solution to workproperly, you must be sure that the AutoYield Property isset to .F. Otherwise, the events will never be surfaced inVFP. This task is taken care of in the Load event of theform. The AutoYield setting is reset to the default in theDestroy event.

ConclusionOnce again, the joining of VFP, Visual Basic, and ADOcan prove to be a powerful combination. Using a VisualBasic-created ActiveX control provides a way to bringfunctionality that doesn’t exist in VFP. For this reason,it’s more important than ever to go beyond VFP and giveserious consideration to the other tools Visual Studiohas to offer.

Language aside, this article brings to light issuesto ponder when considering a design. Clearly, whenpossible, make use of events to centralize code—asopposed to making redundant method calls. From amaintenance standpoint, you’ll definitely save yourself

a lot of work. Finally, I want to take this opportunity to thank my

writing partner and friend Rod Paddock for being a greatsounding board for this article. This is definitely new anduncharted territory. Rod’s assistance was invaluable whenI needed to negotiate the rough spots. ▲

12PETERS.ZIP at www.pinpub.com/foxtalk

John V. Petersen, MBA, is vice president of IDT Marketing Systems and

Services, a Philadelphia-based marketing database consulting firm. John

has presented at numerous developer conferences, including DevCon 97,

Tech-Ed, and the Southwest Visual FoxPro Conference. John is a Microsoft

Most Valuable Professional (MVP) and a co-author of Developing Visual

FoxPro 5.0 Enterprise Applications and Hands-On Visual Basic 5—Web

Development, both from Prima Publishing. [email protected].

Figure 4. The live VFP form implementing the twoActiveX controls.

http://www.pinpub.com 19FoxTalk December 1998

The Kit Box FFFoooxxxTalk

Second Star on the Rightand Straight on ’til MorningPaul Maskens and Andy Kramek 6.06.0

This month, Paul and Andy take a look at naming andobject referencing.

Paul: I was reading through threads on CompuServerecently and picked up a few ideas from there, things thatseem to be causing people trouble, that we can deal withhere. There appears to be a general confusion about whereto put the code in a VFP application and how to refer tothe user interface controls in that code. The power andflexibility of VFP doesn’t help either, as there’s no oneright way to do things.

Andy: Let’s start with naming and referencing in thiscolumn. We’ll worry about where to put the code nexttime. The first thing is to give things meaningful names.Imagine a form with 25 text boxes named Text1 to Text30randomly (with some numbers missing, of course).

Paul: I don’t have to imagine it, I’ve seen it! Not only that,but every form in the application had a Name of “Form1,”which didn’t make life any easier.

Andy: So here’s a guideline for you, then. Name yourforms with the same name as their SCX (that has to beunique within the application anyway), and add to yourbasic form class a Label named lblFormName that has in itsINIT() method THIS.caption = THISFORM.Name. Nowwhenever you run your form, you have a little labelshowing the actual name of the form, and when the userencounters an error, he or she can tell you which form theerror was in, with a name that means something to you.

Paul: You’re presuming that we use a form class, ofcourse. (We ought to return to that point some time, too.)For now, can we just assume things are simple? Evenusing VFP base classes, as many beginners do.

For example, take a form with a text box and aCommand button. We’re obviously agreed that the firstthing is to give them names so you can refer to them.Accepting the VFP default of Form1, Text1, andCommand1 isn’t going to be very helpful in the longterm! So once they have names like frmCustomer,txtName, and cmdSearch, the function of this form andthe controls on it begin to be self-explanatory.

Andy: I would emphasize that using names isn’t asubstitute for adding comments, no matter how clearthose names are!

Paul: Yes! There’s a much under-used form propertynamed Comment in the VFP base class. Like the Tagproperty, this property is never addressed directly by VFPitself. It’s entirely for developer use. Typically, Commentis for, well, comments, and Tag is for data.

I want to keep this simple, so let’s have the buttonchange the background color of the text box. Now wehave a form named frmDemo. No, er, it’s the first demo ofmany. So let’s try frmDemo1. Whoops, we’re back to theText1-Text30 problem described earlier. Uh, how aboutfrmColourChangingDemo?

Andy: See, naming things is harder than writing the code!Just call it frmColChg—that means we don’t need to argueabout how to spell “color.”

Paul: Okay, so following on from that, we could havecmdChgCol to change the color, and txtShoCol that showsthe color change. The code is really easy now. In theClick() event of cmdChgCol, it’s just one line of code:THISFORM.txtShoCol.backcolor = RGB(255,0,0).

Andy: That’s fine. It’s an example of indirect referencingthat will work as long as both objects are on the form.Now how about making this very useful tool reusable bycreating a class and putting the class on the form insteadof the individual controls. Just select both of the controlsand choose Save As Class from the File menu. Name theclass and the library to store it in. Then delete the controlsand add an instance of the new class to the form.

Paul: What about the name of this new class? Andwhat’s the name of this class’s instance on the form? Ireckon cntChgCol is pretty good for the class, and to makematters a little confusing, I think it’s a good name for theinstance, too.

Oh! Once I did that, everything fell apart, though.Running the form produces an “Unknown member‘txtShoCol’ (Error 1925)”. I know it’s there—I can see it,and its name is still txtShoCol in the form designer. But

20 http://www.pinpub.comFoxTalk December 1998

there’s now a container in the way, so there isn’t atxtShoCol object on the form anymore. Instead it’scontained in the cntChgCol, which is on the form, so thereference now has to be THISFORM.cntChgCol.txtShoColinstead.

Andy: Or even better, use THIS.Parent.txtShoCol instead.In fact, that’s what you probably should have done thefirst time, planning ahead in case the group of controlswas going to be reused in a container class, or you weregoing to add them to a Page Frame to the form.

Paul: That reminds me—in MS Knowledge Base articleQ155013, there’s a text box in a column of a grid in acontainer that’s doing an incremental search. In theKeyPress event, there’s a line of code that seems alittle excessive:

THIS.Parent.Parent.Parent.Search( nKeyCode )

I can work out what it’s doing—the Search() methodis a method of the container, and this is passing thekeystroke to it, but that’s not very clear, is it? It’s unlikelyto be broken when it’s reused, because the article is aboutbuilding a custom control.

Andy: So what are you suggesting? In that particular case,I don’t see that there’s any alternative. The referencing hasto be relative, because the text box can’t possibly knowwhat the name of the outermost container will be atruntime. There’s no “THISCONTAINER” reference, andanything relative to “THISFORM” is obviously notapplicable. It’s not pretty, but I don’t see what else youcan do.

Paul: Well, what I’d do is create a ThisContainer reference.Either as a property if I’m using a class, which is set toTHIS.Parent.Parent.Parent in the INIT() method, or as aLOCAL variable that has to be set every time theKeyPress method is called (and therefore is of littlebenefit). There’s a marginal benefit in the latterapproach, because the reference is also used inTHIS.Parent.Parent.Parent.Pop() in the same methodwhere the variable can be used as well.

Andy: I see. So you’re proposing to replace this:

IF nKeyCode = 13 && They hit the ENTER key This.Parent.Parent.Parent.Pop()ENDIFIF (nKeyCode > 48 AND nKeyCode < 58) OR ; (nKeyCode > 64 AND nKeyCode < 123) ** Calls the search method if you hit a ** letter or numeric key. This.Parent.Parent.Parent.Search(nKeyCode)ENDIF

with this:

LOCAL loThisContainerloThisContainer = This.Parent.Parent.Parent

IF nKeyCode = 13 && They hit the ENTER key loThisContainer.Pop()ENDIFIF (nKeyCode > 48 AND nKeyCode < 58) OR ; (nKeyCode > 64 AND nKeyCode < 123) ** Calls the search method if you hit a ** letter or numeric key. loThisContainer.Search(nKeyCode)ENDIF

As you say, the speed benefit is marginal, but thecode is now much more readable and hence easier tomaintain. I’m wondering why use a LOCAL variableif you can’t create a property, when you could use aPUBLIC variable to simulate the property and initializeit by placing goThisContainer = THIS in the container’sINIT() method.

Paul: Aaargh! Splutter, cough . . . You’re playing devil’sadvocate, aren’t you? Well, since there can be only oneglobal variable, you’ll run into problems if your forms aremodeless and can have multiple instances open in theapplication at a time—or even if you adopt this techniquein every container class and just have two or morecontainers on the same form.

Andy: All right! I give in; even though my forms wouldprobably be modal anyway, the second reason is theclincher. (Just testing <g>.)

http://www.pinpub.com 21FoxTalk December 1998

Paul: I could have used WITH ... ENDWITH instead ofcreating a variable, like this:

WITH This.Parent.Parent.Parent IF nKeyCode = 13 && They hit the ENTER key .Pop() ENDIF IF (nKeyCode > 48 AND nKeyCode < 58) OR ; (nKeyCode > 64 AND nKeyCode < 123) ** Calls the search method if you hit a ** letter or numeric key .Search(nKeyCode) ENDIFENDWITH

But that doesn’t make it any more legible. By the timeI’m reading that second .Search() method call, it’s easy toforget where that method is. Besides, I find that leavingoff the leading period is the smallest, hardest to find, mostcatastrophic typing mistake that I regularly make.

Andy: So can we derive a general rule here? How aboutthis: Multiple layers of referencing should be resolved as fewtimes as possible, preferably only once.

Paul: I’d add that in doing so you should keep thecode readable, either by commenting it properly(Andy: shocked gasp!) or using a self-documentingobject reference.

Andy: That’s all very well for your simple example, but inreal life things are a lot more complicated. In order tosend messages between objects, you have to know theirrelative positions in the object hierarchy so that you canwork out the addressing properly.

Paul: What exactly do you mean by the “objecthierarchy”? I’m familiar with the “class hierarchy,”which defines the inheritance relationship, but I’m notquite sure what you mean by the object hierarchy.

Andy: Well, I guess another name for it is the“containership hierarchy”—though I don’t really like theterm because it implies that it deals with “containers”specifically, whereas we’re talking about all objects,whether they’re in containers or not. It refers to therelative positions of objects within the principal container(typically the Form) and describes the relationshipsbetween them. In order to reference one object fromanother, you need only determine the position of each inthe hierarchy to be able to work out how to pass amessage between the two. This is easier to illustrate thanto explain, so take a look at the form shown in Figure 1.

Paul: Okay, I call it the containership hierarchy because Iusually need to use it when I’m messing with containers.That form looks pretty standard (though I wouldn’t havelaid it out that way <g>); what’s your point?

Andy: Well, this form is constructed from a mixture ofclasses and individual objects, so the messaging getspretty complex. For example, the “Personal Mobile PhoneNumber” field is named txtPersMobi, but it’s not a directmember of the form. It’s actually contained in an objectderived from the class cntFullAdd, which is itself acomposite class made up of two other classes—cntAddressand cntContact—and the field in question is defined as amember of the latter.

Paul: Hmm, I think I see what you’re getting at. Theaddress to get that field’s value is actually going to be:

ThisForm.cntFullAdd.cntContact.txtPersMobi.Value

But it’s not exactly obvious, is it?

Andy: Precisely! Especially when you consider that theFind button actually is a direct member of the form (itsapparent container is actually just a “shape” object),whereas the Save button is a member of the cntFormModeclass. Okay, I admit this is a pretty contrived illustration,but I’ve seen more errors and “bugs” in my own (andother people’s) code because the object hierarchy isn’tproperly understood than I care to think of.

Paul: Oh, I don’t know that it’s all that contrived. I’ve seenplenty of examples of forms that mix things up like this,and yes, I agree, it’s always a problem trying to work outhow things relate—especially if you didn’t do the workyourself in the first place. I assume you have a solution,since you’re raising the question?

Andy: Yes, I have. It’s really terribly simple and yet againillustrates how object-oriented programming reflects thereal world. See if you can guess it—after all, what we’retalking about is how to ensure that someone who wants tonavigate within your form doesn’t get lost. What would

Figure 1. Sample contact manager form.

22 http://www.pinpub.comFoxTalk December 1998

you do to ensure that I don’t get lost when I’m coming toyour new offices for the very first time?

Paul: Easy, draw you a map!

Andy: Give the man a cigar! So why don’t we apply thissolution to this case? Figure 2 shows a map of the objecthierarchy for the form shown previously.

Paul: That’s pretty neat. It’s just a standard OrganizationChart, isn’t it? It looks rather like the Object modeldiagrams that MS produces for what used to be OLEAutomation (I can’t remember what it’s called this week).

Andy: Yes, it was produced using the MS OrganizationChart 2.0 add-on that ships with Office, and you can useit to work out how to reference things correctly. All youneed to do is start “walking” from the initial object(“This”) toward the object you want to address. Eachtime you meet a shaded object, you must add that tothe address—using “Parent” if you’re climbing up the

Figure 2. Object hierarchy diagram.

tree, and the object’s name if you’regoing down.

Paul: Okay—let’s suppose I want theSave button to read the person’s“Known As” name. I start from thebutton with ‘This’, then moveupwards and find myself at thecntFormMode object, which is shaded,so I add a parent reference to get‘This.Parent’.

Carrying on upwards, I reach theform object (also shaded, so I needanother “parent”) and get‘This.Parent.Parent’ before startingdownwards again to arrive atcntName—which just gets added:‘This.Parent.Parent.cntName’. Finally,I reach the target text box, so my fulladdress (for the Value property) is‘This.Parent.Parent.cntName.txtKnownAs.Value’.

Andy: Correct. Of course, you canalso take a shortcut, since you can seefrom the object hierarchy that youhave to get all the way up to the formbefore you can start downwardsagain toward your objective. Soyou can simply start directlyfrom the form using “ThisForm”,which would give you theaddress ‘ThisForm.cntName.txtKnownAs.Value’. Figure 3. Improved object hierarchy diagram.

Paul: There’s one thing your diagram doesn’t makeclear—why can’t I go directly from cntFormMode tocntName without passing through the form? There’s a linebetween them, isn’t there?

Andy: No, there isn’t. Remember, we’re dealing with ahierarchy, so you can only go up or down. In reality,there’s no direct connection between the two containers—it’s just drawn that way for simplicity. Each object in thetop row is actually connected directly, and only, to theform. “All roads lead to Form” <g>. Similarly, to get fromone text box in a container to another in the samecontainer, you must go up to the container level beforestarting back down again. I suppose a more correctrepresentation of the situation would look like Figure 3(I’ve added a few other objects so that you can seehow they fit).

Paul: Yes, that’s quite clear now. I do things slightlydifferently. Because I don’t always work these things outin advance, what I often do is print out the form and then

http://www.pinpub.com 23FoxTalk December 1998

write on it. I keep printouts of the form anyway fordocumentation, so an extra one for my reference is nogreat effort. While I’m in the form designer, I just pressPrtSc on the keyboard, Alt-Tab to paintbrush, andCtrl-V to paste in the screenshot, then print it landscape.Then I get out the four colored pens and draw boxesaround the containers, write in their names, and write thenames of controls. Then I might even write in the namesof methods too, because there are occasions when theinterface design is done with the customer before the restof the design is finalized.

Andy: That’s a useful little technique; I’d better trythat out. Doing it in the designer means that you’ll getthe names of all but the very smallest objects as partof the printout, too—nice! (I’ve always done it usinga proprietary screen capture tool with the formrunning; I think I forgot about the PrtSc key when Igave up DOS).

Paul: So do you want to summarize this whole businessneatly now?

Andy: I think so; let’s see. When Wendy Darling askedPeter Pan how to get to Never-Never Land, he drew her amental map and used names that meant something to her.We need to follow the same approach when working withforms and containers. Name the objects meaningfully,and use some sort of visualization of the entire objecthierarchy to ensure that we address objects properly.Doesn’t seem too difficult, really, does it?

Paul: Nope, not difficult at all. One small difference,though—in FoxPro, if you don’t have a map, yourprogram ends up in Never-Never Land. While you’rethere, if you see a bag of marbles about, they’re mine!(clap, clap, clap, clap, clap, I do believe in FoxPro!) ▲

Paul Maskens is a VFP specialist and FoxPro MVP who works as

systems engineer for AZURE IT Ltd., based in Oxford, England.

[email protected].

Andy Kramek is an old FoxPro developer and FoxPro MVP who

works as an independent contractor based in Birmingham, England.

[email protected].

Downloads December Subscriber Downloads• 12COASC2.ZIP—Source code described in Andrew Coates’s

article, “Un-Mapping Mapped Network Drives.”

• 12DONNSC.ZIP—Source code described in Jefferey

Donnici’s article, “Seeing Patterns: The Mediator.”

• 12DHENSC.ZIP—Source code described in Doug Hennig’s

article, “Mining for Gold in the FFC.”

• 12PETERS.ZIP—Source code described in John Petersen’s

article, “Integrating ADO and Visual Basic into Your VFP

Applications, Part 2.”

Extended Articles

• 12SETTI.HTM—Stephen Settimi’s article, “Creating a Set of

Business Rules and the Making of a BusinessRule Server.” In

this article, Stephen first shows how to visualize a business

object as a collection of smaller objects. Once the objects

are clearly identified and defined (the latter isn’t covered in

this article), it’s time to set out those business objects that

might need rules, define those rules, collect them, and then

execute them through a BusinessRule Server and maintain

them as discrete object components.

• 12SETTI.ZIP—Source code described in Stephen Settimi’s

article, “Creating a Set of Business Rules and the Making of a

BusinessRule Server.”

• 12BOOTH.HTM—Jim Booth’s article, “What’s Really Inside:

To Open the Tables or Not to Open the Tables—That is

the Question.” Have you ever wondered if there’s any benefit

to opening the tables for a SQL SELECT command before

you execute the SELECT? Does the SELECT use the source

tables that are already open? Interesting questions. Here

are the answers.

• 12BERG.HTM—“To PrintScreen or Not to PrintScreen.” Art

Bergquist’s follow-up to his September article, “The Visual

FoxPro Screen Image Printer.”

Earn Money!. . . and see your name in print

Go ahead, add a line to your résumé—“Published Articles.”If your tip shows up in the pages of FoxTalk,

we’ll send you $25. See the back page for theaddress where you can send your tips.

24 http://www.pinpub.comFoxTalk December 1998

UUUser nameser nameser name

PPPasswasswassworororddd

yield

zealot

The Subscriber Downloads portion of the FoxTalk Web site is available to paidsubscribers only. To access the files, go to www.pinpub.com/foxtalk, click on“Subscriber Downloads,” select the file(s) you want from this issue, and enter theuser name and password at right when prompted.

Direct all editorial, advertising, or subscription-related questions to Pinnacle Publishing, Inc.:

1-800-788-1900 or 770-565-1763Fax: 770-565-8232

Pinnacle Publishing, Inc.PO Box 72255

Marietta, GA 30007-2255

E-mail: [email protected]

Pinnacle Web Site: http://www.pinpub.com

FoxPro technical support:Call Microsoft at 206-635-7191 (Windows)

or 206-635-7192 (Macintosh)

Subscription rates:United States: One year (12 issues): $179; two years (24 issues): $259

Canada:* One year: $194; two years: $289Other:* One year: $199; two years: $299

Single issue rate: $17.50 ($20 in Canada; $22.50 outside North America)*

Editor Whil Hentzen; Editorial Advisory Board ScottMalinowski, Walter Loughney, Les Pinter,

Ken Levy; Publisher Robert Williford;Vice President/General Manager Connie Austin;

Managing Editor Heidi Frost; Copy Editor Farion Grove

European newsletter orders:Tomalin Associates, Unit 22, The Bardfield Centre,

Braintree Road, Great Bardfield,Essex CM7 4SL, United Kingdom.

Phone: +44 1371 811299. Fax: +44 1371 811283.E-mail: [email protected].

Australian newsletter orders:Ashpoint Pty., Ltd., 9 Arthur Street,

Dover Heights, N.S.W. 2030, Australia.Phone: +61 2-9371-7399. Fax: +61 2-9371-0180.

E-mail: [email protected]: http://www.ashpoint.com.au

* Funds must be in U.S. currency.

FoxTalk Subscription Information:1-800-788-1900 or http://www.pinpub.com

Online Subscription Options Available!Electronic Access PackageMove closer to your dream of the paperless office bysubscribing to our electronic access package. This optionincludes unlimited online access to each monthly issue ofthe newsletter (including Subscriber Downloads) for theterm of your subscription as well as the entire FoxTalkarchive (1996-present). The search capability helps you findanswers fast. To subscribe, go to www.pinpub.com/foxtalk/subscrib.htm or call 1-800-788-1900 to speak to a customerservice representative.

Deluxe PackageThis option includes regular delivery of your print copies ofthe newsletter as well as online access to both the newsletterand Subscriber Downloads content. You’ll be able to accesseach monthly issue on the Web before it even hits the mail as

well as obtain unlimited access to the entire FoxTalk archive(1996-present). The search engine makes looking forinformation on any topic a breeze. To subscribe, goto www.pinpub.com/foxtalk/subscrib.htm or call1-800-788-1900.

Developer Solutions–OnlineIf you haven’t already done so, go to www.pinpub.comand click on Developer Solutions–Online to check out theFoxTalk index of articles for free. (Tell your colleagues!)You can browse the entire index or enter a specific searchterm to pinpoint all of the related articles. If you don’talready have the material you need on hand, you maypurchase individual articles (including the SubscriberDownload file, if applicable) for $5 each online immediately.Visual FoxPro tips are free! No subscription required.

Help us compile valuable salary information for you by participating in Pinnacle Publishing’s anonymous, online salary survey.

Simply go to http://www.pinpub.com/foxtalk/FTsalfrm.htm (you don’t need to be a subscriber) and

take two or three minutes to complete the survey. Watch for the results soon!

FoxTalk (ISSN 1042-6302) is published monthly (12 times per year)by Pinnacle Publishing, Inc., 1503 Johnson Ferry Road, Suite 100,Marietta, GA 30062. The subscription price of domesticsubscriptions is: 12 issues, $179; 24 issues, $259. POSTMASTER: Sendaddress changes to FoxTalk, PO Box 72255, Marietta, GA 30007-2255.

Copyright © 1998 by Pinnacle Publishing, Inc. All rights reserved. Nopart of this periodical may be used or reproduced in any fashionwhatsoever (except in the case of brief quotations embodied incritical articles and reviews) without the prior written consent ofPinnacle Publishing, Inc. Printed in the United States of America.

Brand and product names are trademarks or registered trademarksof their respective holders. Microsoft is a registered trademark ofMicrosoft Corporation. The Fox Head logo, FoxBASE+, FoxPro, andVisual FoxPro are registered trademarks of Microsoft Corporation.FoxTalk is an independent publication not affiliated with MicrosoftCorporation. Microsoft Corporation is not responsible in any way forthe editorial policy or other contents of the publication.

This publication is intended as a general guide. It covers a highlytechnical and complex subject and should not be used for makingdecisions concerning specific products or applications. This

publication is sold as is, without warranty of any kind, either expressor implied, respecting the contents of this publication, includingbut not limited to implied warranties for the publication,performance, quality, merchantability, or fitness for any particularpurpose. Pinnacle Publishing, Inc., shall not be liable to thepurchaser or any other person or entity with respect to any liability,loss, or damage caused or alleged to be caused directly or indirectlyby this publication. Articles published in FoxTalk reflect the views oftheir authors; they may or may not reflect the view of PinnaclePublishing, Inc. Inclusion of advertising inserts does not constitutean endorsement by Pinnacle Publishing, Inc. or FoxTalk.