45
J uly 1996 Parse Strings Delimited with Unusual Characters Into An Array Steve Zimmelman (1) Occasionally you'll get files from a customer or department that are delimited with a non-standard character, such as a carat or a percent sign instead of the usual quotes and comma combination. Here's how to handle these files with little fuss. Recently I had to convert some data files for a client that were delimited with a caret (^) instead of quotes and a comma. The APPEND FROM <file> DELIMITED WITH ^ didn't work because FoxPro still looks for the comma between fields. So I was forced to come up with my own solution to parse out the fields from each line of the text file. I came with the function Str2Array(). With Str2Array(), all you need to do is pass the delimited string, the name of the array you want the information parsed into, and the character that was used as the delimiter. The function parses the fields out of the string and stuffs them into the array elements. After that, it's a simple matter of sequentially stepping through the table structure and replacing the fields with their corresponding array element. Create a temporary table to hold the data just as you would if you were going to use the APPEND FROM <file> DELIMTED command. You can use the FOPEN(), FSEEK(), and FGETS() functions to read the text file. Here's a sample program that uses STR2ARRAY for parsing the text file passed to it. [See Tin-Shan T. Chau's article "Process Text Files Easier" in the June 1996 issue. -- Ed.]:

Parse Strings Delimited with Unusual Characters Into An …portal.dfpug.de/dfpug/Dokumente/FoxTalk/PDF2000/FT0796.pdf · Parse Strings Delimited with Unusual Characters Into An Array

Embed Size (px)

Citation preview

July 1996Parse Strings Delimited with UnusualCharacters Into An Array Steve Zimmelman

(1)

Occasionally you'll get files from a customer or department that are delimited with a non-standardcharacter, such as a carat or a percent sign instead of the usual quotes and comma combination.Here's how to handle these files with little fuss.

Recently I had to convert some data files for a client that were delimited with a caret (^) insteadof quotes and a comma. The APPEND FROM <file> DELIMITED WITH ^ didn't work becauseFoxPro still looks for the comma between fields. So I was forced to come up with my ownsolution to parse out the fields from each line of the text file.

I came with the function Str2Array(). With Str2Array(), all you need to do is pass the delimitedstring, the name of the array you want the information parsed into, and the character that wasused as the delimiter. The function parses the fields out of the string and stuffs them into thearray elements. After that, it's a simple matter of sequentially stepping through the table structureand replacing the fields with their corresponding array element.

Create a temporary table to hold the data just as you would if you were going to use theAPPEND FROM <file> DELIMTED command. You can use the FOPEN(), FSEEK(), andFGETS() functions to read the text file.

Here's a sample program that uses STR2ARRAY for parsing the text file passed to it. [SeeTin-Shan T. Chau's article "Process Text Files Easier" in the June 1996 issue. -- Ed.]:

* CALLER.PRG* the following code sets up the values used to* pass to STR2ARRAY

*-- Text file name to append from --*m.cTextFile = 'WSRECALL.TXT'm.nFile_handle = FOPEN(m.cTextFile)**-- Table To Append Into --*cAppendFile = "RECTEMP"IF ! USED(m.cAppendFile) USE (m.cAppendFile) EXCLUSIVEENDIF*DO WHILE ! FEOF(m.nfile_handle) *-- Get string from text file --* m.cString = FGETS(m.nfile_handle) * *-- Initialize the ArrayName to use --* aTemp = "" * *-- parse string into array --* *-- pass array by reference with @ --* * =Str2Array(m.cString,@aTemp,"^") * *-- Initalize Field MemVars --* *-- and stuff them with array --* *-- elements. --* FOR m.x = 1 TO ALEN(aTemp) m.cfld = FIELD(m.x) DO CASE CASE TYPE(FIELD(m.x)) = "D" &cFld = CTOD(aTemp[m.x])

CASE TYPE(FIELD(m.x)) = "N" &cFld = VAL(aTemp[m.x])

CASE TYPE(FIELD(m.x)) $ "CM" &cFld = aTemp[m.x] ENDCASE ENDFOR *-- Add record --* INSERT INTO (m.cAppendFile) FROM MEMVARENDDO*-- Close Text File --*=FCLOSE(m.nFile_handle)

*******************************************************FUNCTION Str2ArrayPARAMETER m.cStr,aItems,m.cDelimeter*PRIVATE m.cStr,aItems,m.cDelimeter,; m.nItemCount,m.x,m.nLastPos*

It C t OCCURS( D li t St )+1

This is the syntax for Str2Array():

= Str2Array( <Delimited String>,; <@Array To parse into>,; <Delimited Character> )

Be sure to Initialize the array variable you want to use before calling Str2Array().

Steve Zimmelman is a systems analyst programmer for the Christian Broadcasting Network in VirginiaBeach, Virginia, and the principal of ByteSize Software, a software development company. CompuServe74243,1541.

A Modest Proposal Whil HentzenWarning! The opinions in this column are specifically my own and do not reflect the views ofPinnacle Publishing, or possibly anyone else in the Seattle area. But I suspect one or two of youout there might end up nodding in agreement.

Remember the old days? Specifically, back in the late 1980s? Those were the days whencompanies worldwide were using dBASE III+ and IV, and a few daring souls, tired of their userswhining about sluggish performance in their desktop database applications, resorted to a newtactic. They compiled the dBASE application with this new product from a tiny little companysomewhere in the Midwest, and demonstrated the same functionality (and source code), albeitwith better response time. Well, "better response time" might be understating the effect; sockswere flying everywhere as a result of being blown off the users.

Of course, then MIS stepped in and ask "How'd you do that?" and, shyly (or, in some cases,boastfully) the developer would admit "I just compiled the finished application in FoxBASE."

"Oh, we can't have that! It's not on the approved list!" MIS would roar. The user, on the otherhand, ignored the argument, pleased as punch that they could finally get some decentperformance out of the system, and, as you guessed, the developer ended up with anotherconvert.

This scenario played out over and over, and the little company in the Midwest grew bigger, mademore money, and released new, more popular versions. Meanwhile, competitive releases ofdBASE were increasingly bug-ridden and lost market share. Eventually, the largest desktopsoftware manufacturer in the world bought Fox Software and dBASE disappeared as a viablecompetitor.

Well, it's been nearly a half-year since the recent hubbub from those industry rags arose, and let's

face it, FoxPro has been getting an unfair, undeserved bad rap in companies all over the world.People are shying away from the product on the basis of gossip and innuendo, completelyignoring the capabilities and functionality of the product. And it's obvious that we're not going toget the marketing support we need from Microsoft. The position of the entire FoxPro marketingteam is embodied in the following quote: "It's my job to promote FoxPro, but not at the expenseof other Microsoft products." I'll discuss the fallacy in this point of view in a future article, buthere and now let me make a modest proposal.

First, pick up a copy of Visual Basic and spend a bit of time with it. Get acquainted with theinterface (guess what? The only learning curve you'll have is to remember what you can't do inVB that's a piece of cake in VFP), and learn how to access data. By the time you're done, you'llhave a better appreciation of just how good you have it with FoxPro, as well as realize that VB isa better tool for some types of applications. (Face it, there are no good games written in FoxPro.)

Pick out an application that requires a moderately sized data set, say 250,000500,000 records.Write it in both FoxPro and VB. Make sure the interface is reasonably similar, and that it hideswhich language you wrote the application in. Have a VB buddy of yours rip your VB code apartto make sure that it's reasonably sound -- that you didn't commit the VB equivalent of doing aLOCATE without an appropriate index tag -- and it's time to visit some customers.

Show the VB version and demonstrate some cool features. Get them comfortable that this is areasonable VB application -- one that they could expect from your typical VB developer.

Then, pull up the FoxPro version. They'll quickly notice the difference in performance, and youcan probably squeeze in a mention of the percentage of development time it took.

Well, now the tough part comes. You'll have to explain why the Microsoft marketing machineprefers to sell VB over FoxPro. And since there really isn't any rational explanation, you'll haveto leave it to the customer to figure it out. "Let's see, I can have this written in FoxPro and it willcost me next to nothing for Microsoft products since you can deliver a standalone .EXE, or I cando this in VB and it will take longer, have much poorer performance, but still cost me very littlein software outlays, or I can do this in VB and SQL Server, and it will take longer, haveacceptable performance, and I have to spend how much for Microsoft software?"

This is called guerrilla marketing. It got us to where we are today, and before getting completelyfrustrated at the lack of support from Microsoft's marketing machine, remember what we weretold last year at Database World: "I expect you developers to market the product for us!"

Remember, this is just a modest proposal. But we've been doing this all summer, with somesurprising results. And I swear I'm not making this up.

Connect FoxPro 2.6 to a Web Server Paul Russell

(2)

Last month, FoxTalk brought you a discussion by Rick Strahl on creating database applications forthe Web using Visual FoxPro. Lest you think that you're left out in the cold if you're still usingolder versions of FoxPro, Paul Russell shows us how to connect our legacy applications to a Webserver using FoxPro for Windows instead.

Although plenty of data is available on the Internet, not much that can be done with it in its rawform. One of our customers, for example, has a FoxPro for Windows application that issues a 5Dquery on over 500M of geographic data. This is too much data for people to handle in its rawformat, and it's certainly too much to send across the Internet. However, the data can be passedthrough a query program and the results can be sent across a lot more reasonably. So we had tofind a way to connect the legacy FoxPro (both DOS and Windows) application to a Web serverso that people around the world could submit queries and retrieve the results.

Query guidelines

A number of restrictions had to be observed with this project. Some of these were because of thenature of the query and equipment, and some were because of the business or computingenvironment. Here's a short list of the guidelines and requirements that I had to deal with whendeveloping this site:

• The site had to run on a Windows NT server, using the Netscape server software.

• The query processor had to run in FoxPro for Windows.

• No FoxPro for Windows program could run for more than a few seconds at a time. Thisis so that the user didn't lose interest and click on the [Stop] button.

• Other than the server software, there could be no expense for software. All software usedhad to be either developed internally or in the public domain.

• Although multiple queries could be submitted simultaneously, only a single query couldbe processed at any one time. This was so that multiple query processes didn't bog downthe server performance.

• If at all possible, the NT server should be set up so that no one ever has to log into it.

• The user had to receive some sort of notification that the query had been processed, andthey had to be able to retrieve the results (it isn't uncommon for a single query to take 10minutes or more to be processed).

• The query request had to be validated and the user had to receive immediate feedback onwhether or not the request had been accepted.

• Because a query could take a long time to process, the project authority didn't want freeaccess to the site for all users. This would prevent casual visitors from running multiplequeries "just because they could." Some form of security had to be implemented.

• However, the project authority also wanted to give the user a bit of information,

including some place to request authorization. This meant that there had to be a "homepage" for the site.

The first challenge

I'm fortunate to work for a company with lots of Internet experience. The first thing that I had todo with this project was learn enough HTML to create the initial page and enter through theWeb. I worked for about a half a day with one of our Web folks to put the basic form together.This was quite easy once we got going.

Now that I had the information in the form, I had to find a way to get it into FoxPro, and then getFoxPro to return results to the client. I decided to take things one step at a time, and get resultsback to the server any way possible. Once I had done that, I would move on to get the resultsfrom FoxPro.

The standard way to pass information between a Web client and server, process the informationon the server, and then return results to the client, is through a mechanism called CommonGateway Interface (CGI ). A CGI program (which is usually referred to as a CGI script becausemost Web servers have been UNIX based) receives the form information as a single operatingsystem environment variable, and returns the information to the server simply by printing it toStdOut. This works quite well with a UNIX-based Web server, but it breaks down both in theWindows world and in the FoxPro world. Windows has no concept of StdIn or StdOut, and itsimply can't print variables to the screen. Coupled with that is the fact that FoxPro displaysinformation to a memory-mapped display, completely bypassing the standard TTY routines thatare built into the PC (FoxPro has always done it this way, but you could get FoxBASE+ to printto the screen as a TTY device -- remember the -notibm switch?).

Since CGI is, effectively, unavailable to all Windows-based servers, a new standard wasdeveloped. Robert Denny, author of the WebSite product, developed a slightly modified versionof CGI specifically for Windows, and called it Win-CGI. This new standard passes informationto the CGI script through a few environment variables and a couple of configuration files, andinformation is passed back to the server through a third file. This seemed like the onlyreasonable choice for a protocol.

Win-CGI explained

Now that I had a protocol to use, I had to figure out the details. First I had to see whatinformation was passed each way. I ended up with a .BAT file that could possibly be the world'smost simple Win-CGI script. CGI-TEST.BAT retrieves all of the information that the server haspassed from the form and then formats some results to be sent back to the client.

Win-CGI passes three parameters, all filenames, to the CGI script. The first file, which containsthe form information and some other housekeeping variables, is formatted like a standardWindows .INI file. The second file, which contains a single line of information, is formatted thesame way as the environment variable that I mentioned earlier. Both of these files passinformation to the CGI script. The third file is what the CGI script uses to send a valid HTMLpage back to the server. The server creates this file, and then monitors the disk to see when the

file closes. Upon seeing this third file close and the Win-CGI process end, the server reads thefile and hands it back to the client.

Here are the basic steps in a CGI or Win-CGI communications session (see Figure 1):

• The Web Browser submits a request to the server for an HTML page. This is in the formof an URL (Uniform Resource Locator).

• The Web Server sends back the page that the browser has requested.

• After the user has filled out the form on the page, the browser sends the information backto the Web server.

• The server formats the information and hands it to the Win-CGI script

• Most CGI and Win-CGI scripts are data-related, so the Win-CGI script then hits thedatabase.

• The data engine (in this case, FoxPro for Windows) returns the information to the CGIscript.

• The CGI script processes the data that is returned and sends an HTML page back to theserver.

• Lastly, the server returns this HTML page to the browser.

There are nine distinct steps that are performed in CGI-TEST.BAT:

1. Set up the Output File. The server watches the output file and attempts to return controlwhen it first sees it close. For this reason, output from CGI-TEST.BAT is directed to a secondoutput file, and this is eventually copied over the file that the Web server is watching.

2. Indicate the HTML file type. When returning results from a CGI script, the browser has tobe told how to handle the file. The first line, followed by a blank line, does this. TheContent-type has to be a valid MIME (Multiple Internet Message Enclosure) type and is usuallyeither text/html or text/plain.

3. Display the environment.

4. Display the parameters. Although the first three parameters are the only ones used here, all ofthe parameters that DOS can see are output.

5. Display the contents of the current directory.

6. Display the contents of C:\TEMP.The Netscape server creates temporary directories inC:\TEMP on the server, one each time someone executes a Win-CGI script. This directorycontains all of the files for the script and the server deletes it when control is returned to theclient.

7. Display the file in %1. This is the .INI-type file that contains all of the form information.

8. Display the file in %2. This file contains all of the form information as a concatenated string.

9. Copy the temporary file to the permanent file. This closes the file designated as %3 andreturns control to the server. The server then returns the file in %3 to the browser and returnscontrol to the browser.

* cgi_test.bat*@echo offrem CGI-TEST.BAT

set output=output.fil

echo Content-type: text/plain > %output%type blank.line >> %output%

echo Called as a WIN-CGI script >> %output%type blank.line >> %output%

echo Current Environment >> %output%echo ------------------- >> %output%set >> %output%type blank.line >> %output%

echo Parameters >> %output%echo ---------- >> %output%echo %%1 = %1 >> %output%echo %%2 = %2 >> %output%echo %%3 = %3 >> %output%echo %%4 = %4 >> %output%echo %%5 = %5 >> %output%echo %%6 = %6 >> %output%echo %%7 = %7 >> %output%echo %%8 = %8 >> %output%echo %%9 = %9 >> %output%type blank.line >> %output%

echo Current Directory >> %output%echo ----------------- >> %output%dir >> %output%type blank.line >> %output%

echo C:\TEMP Directory >> %output%echo -----------------dir c:\temp /s >> %output%type blank.line >> %output%

echo Data File (%%1) >> %output%echo -------------- >> %output%type %1 >> %output%type blank.line >> %output%

echo Configuration File (%%2) >> %output%echo ----------------------- >> %output%type %2 >> %output%

copy %output% %3

Both CGI-TEST.BAT and sample output (as OUTPUT.FIL) have been included in download file

_RUSSEL.EXE. The file blank.line contains the two characters CR/LF and may or may not beincluded.

Setting up communications with FoxPro

Once basic communications were established, FoxPro could be brought into the picture. TheFoxPro program in this case has three distinct actions to perform: retrieve the form information,communicate with the database, and prepare the result.

Before going further I'd like to point out that Web browsers don't set up a persistent connectionwith whatever back end they are dealing with. The connection exists only while information isbeing requested and then retrieved. When the browser displays "Document: done" at the bottom,the connection with the backend has been closed. One of the difficulties imposed by this is thatthere is no easy way to carry information from one Web page to the next. A standard way aroundthis is to set up "hidden" variables on a form. These are passed to the CGI script as regular formvariables.

Retrieve the form information

I've found that the easiest way to work with form information is to read the data file (the.INI-type file) into two arrays and then work with the arrays themselves. This means that the datafile is read only once, which increases the speed and reduces the complexity of the application.

The first array contains a list of the labels in the data file. Each form element has to have a namespecified with it for the data to be useful, and this name becomes the label. The second arraycontains the values that correspond to the label offsets in the first array. For example, if the datafile contains the following, then the first array would contain the information in Table 1:

[Form Literal]query_name=Sample Query 1user_name=Paul [email protected]:

Table 1. The labels in the data file are contained in an array formatted like this.

laFormLabel(1) "QUERY_NAME" laFormData(1) "Sample Query 1" laFormLabel(2) "USER_NAME" laFormData(2) "Paul Russel" laFormLabel(3) "E-MAIL" laFormData(3) "[email protected]"

To then retrieve the value of the e-mail address, you could use the following:

m.lcEMail = laFormData(ascan(laFormLabel,"E-MAIL"))

ReadForm.PRG and FormData.PRG have been included in download file _RUSSEL.EXE to take

care of this for you. ReadForm reads the data file and populates the two arrays (which must becreated previously), and FormData returns the value from laFormData.

I included FormData (instead of using the previous expression) for one very simple reason: thedata on the form may not exist. If there's a check box on the form and the user checked it, thenthe value in laFormData is "on." If the user hasn't checked the check box, then the label and dataaren't included in the data file. If you then use the previous expression, it will blow up. IfFormData is used to request a value that doesn't exist, an empty string will be returned.

Communicate with the database

Once the information has been retrieved into the arrays, it's a simple matter to construct a SQLstatement or run through a SCAN or other type of loop to access the data, which I'll explain inthe next section.

Prepare the results

There are two types of results that can be returned. The first is whether or not an operation wassuccessful and the second is a list of information. Whatever happens, the information has to bewritten to a file that can be sent back to the client.

For this example, let's assume that the program will list the queries that have been submitted. Inthis case, you want to display, in a table, the following fields: query number, person's name,e-mail address, IP number, date created, date processed, processing time, and number of recordsgenerated. One requirement is that the user has to be able to display either all of the queries, orsome number of queries, and they have to be able to type in the number. Also, since queries areadded to the end of the database, and since you want to display the most recent ones first, youhave to go through the database backwards.

To set up these conditions, create two forms. The first form should have a text field and a button.The user can type the number of queries into the text field, and then when they press the button,the information for that number of queries will be displayed. The second form has a hidden fieldthat contains the word "All" (this hidden field has the same name as the text field on the firstform), and a button. When the user presses the button, all of the queries will be listed.

First, set up the result string. If there were any queries to list, then the result string will containthe field information. If not, it will be empty. Here's the code:

m.crlf = chr(13)+chr(10)

m.lnReturn = 0m.lcReturn = FormData("Quantity")if val(m.lcReturn) # 0 m.lnReturn = val(m.lcReturn)endif

m.lcResults = ""go bottomdo while (m.lcReturn == "All" or m.lnReturn > 0) ;and not bof()m.lcResults = m.lcResults + ;"<TR>"+ ;"<TD ALIGN=RIGHT>+str(qry_list.qry_no,4)+"</TD>"+ ;"<TD>"+padr(qry_list.query_name,25)+"</TD>"+ ;"<TD>"+padr(qry_list.email,30)+"</TD>"+ ;"<TD>"+qry_list.remote_ip+"</TD>"+ ;"<TD>"+dtoc(qry_list.entered_d)+"</TD>"+ ;"<TD>"+dtoc(qry_list.process_d)+"</TD>"+ ;"<TD ALIGN=RIGHT>"+str(floor(qry_list.duration/60))+":"+ ;padr(mod(qry_list.duration,60),2,"0")+"</TD>"+ ;"<TD>"+str(qry_list.records)+"</TD>"+ ;"</TR>"+crlfskip -1m.lnReturn = m.lnReturn - 1enddo

Once the results string has been constructed, the output file can be written:

m.lnFile = fcreate(m.lcFileOut)

if m.lnFile > 0

=fwrite(m.lnFile, ; "Content-type: text/html"+crlf+ ; ""+crlf) =fwrite(m.lnFile, "<HTML>"+crlf+ ; "<HEAD>"+crlf+ ; "<TITLE> List the Queries</TITLE>"+crlf+ ; "</HEAD>"+crlf+ ; "<BODY BGCOLOR=C0C0C0>"+ ; "<

"+crlf ) =fwrite(m.lnFile,; "<FONT SIZE="+2"> List the Queries</FONT>"+crlf+ ; "<P>"+crlf)

if empty(m.lcResults) =fwrite(m.lnFile, ; "No queries were found in the list.") else =fwrite(m.lnFile, ; "<TABLE BORDER=2>"+crlf+ ; m.lcTitle+crlf+ ; m.lcResults+crlf+ ; "</TABLE>") endif

=fwrite(m.lnFile, ; crlf+"<P>"+crlf+ ; "<"+crlf+ ; "</BODY>"+crlf+ ; "</HTML>" )

=fclose(m.lnFile)

copy file &lcFileOut to &lpCopyFile erase (m.lcFileOut) endif

Error handling

The biggest problem with developing a CGI script is that there's no graceful way to handle

errors.

Although the Web seems to be fairly interactive, if something goes wrong with a CGI script,then nothing is returned to the user. In fact, control doesn't even return to the browser. The useris left staring at an hourglass and can only press the Stop sign (to abort the process).Unfortunately, while this aborts the communications and returns control to the browser, the CGIscript is still running on the server. If this is a standard FoxPro error, then it's likely waiting forsome type of user interaction (like pressing the Cancel or Ignore button). The user can never getto that particular FoxPro session, so these buttons can never be pressed. The end result is that anextra FoxPro process is running and tying up server resources, and these resources will be tiedup until the server is re-booted.

So the next thing that has to be developed is a robust error strategy. This has to be something thatwill replace the entire contents of the output with an error message that the developer canunderstand, but will still convey enough meaning to the user.

I have standardized on a few things to help the error handling in this application:

• Have consistent page headers and footers throughout the entire Website. This means thatthe error handler can be set up once in the initialization routine of the script, and it willlook the same no matter where it's called from.

• Use a single variable name as the file number to write the output to. The error handlercan then determine if this file is in the process of being created and, if so, can completelyreplace it. If the output file doesn't yet exist, then the error procedure would create it.

• Display just the program, error number, and line number to help the developer out. Therest of the error page should be formatted to help the user.

• Initialize all of the required variables before the error handler has been set up with ONERROR. When the code moves into maintenance mode, another programmer couldinadvertently insert some code between the error-handler initialization and the variabledefinition. If this code is in error, then the program will crash, and you will get absolutelyno feedback.

• Set up the error handler as close to the top of the program as possible. Set it up once, andleave it.

Conclusion

Some months ago an article in another FoxPro-related magazine questioned the nature of theMicrosoft Certified Professional program. The author couldn't understand why we should beforced to learn something more than our beloved FoxPro. After all, FoxPro can do anything thatwe will ever have to do.

I've always taken exception to this attitude, and this article shows one reason why. There are somany technologies available that understanding only one of them imposes serious restrictions onprofessional and personal development. FoxPro is a wonderful tool for talking to data very

quickly and it features a very complete and powerful language. But there are things that it justcan't do.

We should learn how to use the tools that we know the best, and we should learn enough aboutother technologies so that we can properly decide to: discard the technology altogether,communicate with the technology as we've done here, learn and understand the technology, orrecognize that this technology will be important at some point in the future. Then when we talkto our customers, we can be more confident about providing the best solution for them instead oflimiting them to fit within the capabilities of FoxPro.

Paul Russell is a software specialist working in Halifax, Nova Scotia. [email protected],[email protected], or CompuServe 102440,2125.

Tip: Locate the Windows Directory Oleg Golant (3)

Sometimes in a FoxPro application it's important to know the location of the Windows directory.This becomes of an essence with the increasing number of different versions of Windowsoperating system, namely Windows 3.1, Windows 95, and Windows NT.

The following function returns the name of the Windows directory:

**************************************************** Function: WindDir.PRG* Author: Oleg Golant* Created: 02/13/96* Purpose: Returns Windows directory* Parameters: None* Returns: <expC> - Windows directory, if* success, null string otherwise* Functioning: 1. Loads FoxTools.FLL* 2. Registers and invokes* GetWindowsDirectory API function* 3. Analyzes the result* 4. Returns the value* Example: WAIT "Windows Directory: " + ;* WinDir() WINDOW NOWAIT***************************************************PRIVATE lhGetWinDir, lcWinDir, lnPathSizePRIVATE lcRetVal, llFoxToolsIF 'FOXTOOLS' $ UPPER(SET('LIBRARY')) llFoxTools = .T.ELSE SET LIBRARY TO SYS(2004)+'FOXTOOLS.FLL' ADDITIVE llFoxTools = .F.ENDIFlcWinDir = SPACE(144)lhGetWinDir = RegFn('GetWindowsDirectory','@CI','I')lnPathSize=CallFn(lhGetWinDir, @lcWinDir, LEN(lcWinDir))lcRetVal = ''DO CASECASE lnPathSize == 0 WAIT ALLTRIM(PROGRAM())+": Function failed!" ; WINDOW TIMEOUT 3CASE lnPathSize > LEN(lcWinDir) WAIT ALLTRIM(PROGRAM())+": Buffer is too small!" ; WINDOW TIMEOUT 3OTHERWISE lcRetVal = AddBS(ALLTRIM(lcWinDir)) && Add BackSlash && (FoxTools)ENDCASEIF .NOT. llFoxTools RELEASE LIBRARY SYS(2004)+'FOXTOOLS.FLL'ENDIFRETURN lcRetVal

To use it, make sure WINDIR.PRG is in your path and that FoxPro can also find FOXTOOLS.Then issue a command like this to display a wait window that shows the directory whereWindows is installed:

WAIT "Windows Directory: " + WinDir() WINDOW NOWAIT

Oleg Golant is a senior programmer/analyst for Teachers Insurance and Annuity Association CollegeRetirement Equities Fund (TIAA-CREF) in New York City. 212-916-6080, CompuServe 74727,3400.

Tip: A Beating Heart Tells You theApplication is Still Running Teimour Makhmoudov

I recently needed a utility that could show our users whether the application is running or if it'shung due to a fatal error, such as network, memory, or other environment problems. I developeda simple routine using the Timer control. The icon in the title bar is changed in the Timer eventas the system runs; if the icon stops changing, the system has locked up.

The code in the Timer event is straightforward: a property of the form is set to a number from 1to 4, and each time the timer fires, the property is changed and the icon is changed:

_SCREEN.Icon="heart" + ; ltrim(str(thisform.nHeartSize)) + ".ico"if thisform.nHeartDir=1 thisform.nHeartSize=thisform.nHeartSize + 1else thisform.nHeartSize=thisform.nHeartSize - 1endifif thisform.nHeartSize="4" thisform.nHeartDir=-1endifif thisform.nHeartSize="1" thisform.nHeartDir=1endif

You'll note that the property doesn't cycle in a circular fashion each time; rather, it swings backand forth from 1 up to 4 and then back down to 1 like a pendulum. This gives the appearance ofthe heart "beating" -- getting bigger and then smaller again. Also, it's important to note that theTimer control stops during certain events, such as the user dropping down a menu but notmaking a choice from the menu.

Teimour Makhmoudov is a senior software developer for Market Line Computers in Closter, New Jersey.

Wrappers Steven Black

A decorator allows you to dynamically attach additional responsibilities to an object. They providea flexible alternative to subclassing for extending functionality. Steve examines different sorts ofwrappers, including decorators, and how they can mean different things in procedural andobject-oriented programming.

The term "wrapper" comes up all the time and, depending on the context, can mean differentthings. The different meanings can lead to confusion, so in this article we'll look at wrappers,with a specific focus on object-oriented wrappers. I'll discuss three sorts of wrappers: proceduralwrappers, functional wrappers, and the wrapper object-oriented pattern, which is also known as adecorator.

Procedural wrappers

Procedural wrappers are a commonly used packaging technique that can be used in all versionsof FoxPro, and indeed by most other computer languages. Most developers have used theprocedural wrappers at some point. A procedural wrapper has little to do with object-orientedprogramming (OOP), even though this is what most people first think of when someone says"wrapper".

The idea behind a procedural wrapper is to envelope (wrap) the call to a function or programinside another function or program. Because of hierarchical scoping, the memory variables(except LOCAL variables in Visual FoxPro (VFP) and procedures of the wrapper program canbe referenced and called by the program being wrapped.

Here's a great example of the usefulness of procedural wrappers: In FoxPro 2.6, a goodproductivity tactic is to desnippetize your screens. When you do this, you remove code fromsnippets -- where it's hard to access -- replace it with a function call, and move the code to awrapper program. When the screen executes the snippets, the code in the wrapper is in-scope.

Not long ago there was much discussion about the merits of desnippetizing. On one hand it leadsto far shorter development cycles and easier code maintenance. On the other hand, pro-snippetfolks say the transition to OOP (via a conversion routine) would be easier if the code associatedwith an "object" were kept there.

As it turns out, the automated conversion routines in VFP don't make for very good VFPprograms, much less do they make decent OOP programs, so the point is moot. If you continueto work in FoxPro for Windows or FoxPro for DOS and haven't yet used a wrapper program toremove snippets from your screens, what the heck are you waiting for!?

Functional wrappers

A functional wrapper dresses up a collection of routines to make them look and behave like an

object. For example, consider a procedure file with the following functions. (The actual code forthese are in HOME()+"\GENMENU.PRG").

* FileProc.PRGFUNCTION justfname( filname)* - Return just a filename

FUNCTION justpath( filname)* - Return just the path

FUNCTION stripext( filename)* - Strip the extension

FUNCTION strippath( filename)* - Strip the path from a file name.

The idea behind a functional wrapper is to take procedural code and envelope it a DEFINECLASS statement in order to give it an "object" look. So the previous pseudocode becomes thefollowing:

* FileProc.PRGDEFINE CLASS FileStuff AS CUSTOM FUNCTION justfname( filename) * - Return just a filename

FUNCTION justpath( filename) * - Return just the path name

FUNCTION stripext( filename) * - Strip the file extension

FUNCTION strippath( filename) * - Strip the path from a filename.ENDDEFINE

As you've probably concluded, using the functional wrapper now involves creating an object, soinstead of invoking the following:

*-- Make sure the procedures are in scopeSET PROCEDURE TO FileProclcHomeDir= justpath( SYS(16))

you would instead use this:

*-- Make sure the procedures are in scopeSET PROCEDURE TO FileProcoFile= CREATE( "FileStuff")lcHomeDir= oFile.justpath( SYS(16))

It seems like more typing, so why bother? There are three reasons. First, functional wrappers arebona fides classes, meaning you can subclass them to achieve some variant behavior. Second,different methods can share the same name since the object identifier makes each one distinct.Third, dealing with procedures as objects is much more consistent with the way everything elseworks in VFP.

There is considerable debate about the benefits of using functional wrappers instead ofprocedures (see SPEEDIE-95 in the "References" sidebar), though their functional utility isundeniable. In the end, it boils down to personal preference, but making everything an object isprobably a mistake.

The decorator wrapper pattern

Finally, a decorator wrapper (I'll just call it decorator) is an elegant OOP technique that can behandy in a variety of situations. In particular, the decorator pattern is useful for extending thefunctionality of a class without polluting (or even needing) the original source.

But before I discuss the decorator pattern, let's examine what happens when you allow a class togrow organically with inheritance. To illustrate, consider the following class interface for a frog:

DEFINE CLASS Frog AS CUSTOM FUNCTION Jump(nValue) FUNCTION Eat()ENDDEFINE

Amphibians are easily created with Visual FoxPro. Suppose that you need to create a new sort offrog, one that can dance. You could subclass Frog as follows:

DEFINE CLASS DancingFrog AS Frog FUNCTION Dance()ENDDEFINE

Suppose you later need a singing Frog. Within your current hierarchy, you have several obviousoptions:

• You could simply augment the DancingFrog with a Sing() method, perhaps renaming theclass, if that's possible, to EntertainingFrog to better reflect its enhanced functionality.

• You could subclass the DancingFrog, add a Sing() method to the new subclass, and call

the new class, say, DancingSingingFrog.

• You could insert a new Frog subclass in the hierarchy before DancingFrog, endow it witha Sing() method, and call it, say, SingingDancingFrog.

• You could make all Frogs able to sing by augmenting original Frog class.

• What the heck, you could vastly simplify this rapidly exploding hierarchy by eliminatingthe DancingFrog class and give every Frog the Sing() and Dance() methods in a singleand monolithic Frog class.

So, buffeted by the needs of this current implementation, you make your choice and take yourchances. What if you later needed a burping frog, one that belches after meals? Do you subclassagain? If so, where? Do you add a Burp() method, or do you simply augment the Eat() methodbecause problem analysis indicates that burping naturally follows from eating? Again, whichhierarchical level is best for modification? And later, when you need a frog that can beg,roll-over, shake-a-leg, and stay, a new question eventually arises: "How did this Frog classbecome such a mess?"

At the outset, pragmatic reuse artists would be correct in asking when, if ever, will the need for asinging, dancing, and belching frog arise again? Why not just augment the frog for this instance,without polluting with trivial nuance our general, simple, and reusable Frog class?

The answer may be to use a decorator. A decorator, in essence, is a class with mostly"pass-through" behavior. It "wraps" a class by reference, forwarding all messages to thereference except for the messages the wrapper is designed to intercept. Setting up a decoratortakes a bit of work, but thereafter it's a snap to use. Consider the following DecoFrog class:

DEFINE CLASS DecoFrog AS CUSTOM oRealFrog= .NULL. FUNCTION INIT( oFrog) THIS.oRealFrog= oFrog FUNCTION Jump(n) THIS.oRealFrog.Jump(n) FUNCTION Eat() THIS.oRealFrog.Eat()ENDDEFINE

To the outside world, the DecoFrog class has the same programming interface as a Frog. But it'snot a Frog, it's a lens through which you can "see" a Frog. If you need a specialized one-off Frog,such as one that sings, you could do this:

DEFINE CLASS DecoSingingFrog AS DecoFrog FUNCTION Sing() ? WAIT WINDOW "It's not easy being green..."ENDDEFINE

Similarly, you can define a dancing frog by simply subclassing the class DecoFrog as follows:

DEFINE CLASS DecoDancingFrog AS DecoFrog FUNCTION Dance() DecoFrog::Jump(+1) DecoFrog::Jump(-2) DecoFrog::Jump(+1)ENDDEFINE

Retrofitting a decorator is easy. For example, before your code looked like this:

Kermit=CREATE( "Frog")

Now, a singing frog can be substituted at run time like this:

Kermit=CREATE("Frog")Kermit=CREATE("DecoSingingFrog",Kermit)

or more succinctly:

Kermit=CREATE("DecoSingingFrog",CREATE("Frog"))

(Nesting CREATEOBJECT statements work fine in Visual FoxPro).

Additionally, decorators can be chained, with functionality added or modified as needed. Thisgives you considerable pay-as-you-go flexibility. For example, to build a singing and dancingfrog, do as follows:

Kermit=CREATE("Frog") Kermit=CREATE("DecoSingingFrog",Kermit)Kermit=CREATE("DecoDancingFrog",Kermit)

Or, if you prefer one line of code, use the following:

Kermit=CREATE("DecoDancingFrog", ; CREATE("DecoSingingFrog",CREATE("Frog")))

This creates an object relationship illustrated by the object diagram in Figure 1.

Voilà! You now have an ordinary Frog named Kermit that appears, in this instance, with the

ability to sing and dance, and we didn't need to pollute the Frog class to get it. In fact, we didn'tneed the source to class Frog.

Decorator benefits

Here are benefits that come from using decorators:

• Decorators can be an effective substitute for multiple-inheritance, which isn't supportedin Visual FoxPro. In the Kermit the Frog example, in some languages you could haveused multiple inheritance to combine the Frog class and, say, an Elvis class to produce anentertaining frog. What we did instead is wrap an ordinary Frog object in variousentertainment objects to extend the frog's capability as needed.

• You incur resource expenses on a pay-as-you-go basis because you can decide at runtime what types of objects to create. Also, you don't need to foresee all the futurefunctionality of a class, deferring to decorators as new needs arise.

• You can extend the functionality of a class knowing only its interface; source codeusually isn't required.

• If you use a decorator to endow your classes with one-off characteristics, you can neatlyavoid extending your class hierarchy to support the characteristic.

• In some situations you may be able to use a lightweight decorator -- one that doesn'texpose the whole interface of the wrapped class -- in order to accomplish a simple task.

Decorator downsides

As with all things, the "no free lunch" pattern applies: You can't get the added flexibilityafforded by using a decorator without some tradeoffs. Here are some of them:

• A decorated object isn't the same thing as the object itself. In the Frog example, if the lineTHIS.Eat() appears somewhere within the Frog class, it will call the Frog's Eat() method,and not the Eat() method that may be enhanced by a decorator.

• Decorators are best used with classes that use access functions to expose their properties.Querying and setting decorator properties without pass-through access functions doesn'ttouch the object being decorated.

• It takes more time to instantiate an object if one also instantiates one or more decorators,and once created, it takes slightly more time for messages to filter through layers ofdecorators to reach the object itself.

• People who aren't familiar with decorators are likely to find the proliferation of wrapperobjects confusing.

• A decorator is, in fact, a second distinct class whose interface must be maintained inconcert with the class it decorates. This can be problematic, but is alleviated somewhatby making the decorator a subclass of an abstract class, as in the diagram in Figure 2. In

this way, decorators automatically get new interfaces, allowing you to wire the"pass-through" behavior at a later time. In the class diagrams in and Figure 3, if theabstract class defines the complete interface for the concrete class (as it should), then newdecorators appear automatically.

Conclusion

Decorators change the appearance of an object in a way that is fundamentally different fromsubclassing. Subclassing changes class internals; a decorator changes the class appearance.

Decorators aren't solutions for every situation, but occasionally they're just the ticket. The nexttime you find yourself subclassing for the purposes of a particular implementation, ask yourselfif a decorator wouldn't better suit your situation.

Steven Black is the technical lead at SBC/UP!, a FoxPro consulting company based in Kingston, Ontario,Canada. He specializes in multilingual and multicultural FoxPro development, software projectturnarounds, and other challenges. He is the author of Steven Black's INTL Toolkit, a framework formultilingual FoxPro development in FoxPro 2.x and 3.0, and several third-party GENSCRNX tools.613-542-3293, [email protected], CompuServe 76200,2110.

Sidebar: References Gamma, E., Helm, R., Johnson, R, and Vlissides, J. (1994) Design Patterns, Elements of ObjectOriented Software. Reading, MA. Addison Wesley. ISBN 0-201-63361-2

[SPEEDIE-95] Speedie, D., (1995) To OOP and Back, FoxPro Advisor Magazine April 1996,Advisor Publications. San Diego CA.

Tip: Dynamically Modify Grid ColumnAlignment Mark Nadig

Be careful when dynamically assigning the ControlSource property of a grid's column whenusing default alignment. Evidently, with default alignment, Visual FoxPro determines which wayto align when it's instantiated. If the datatype of the ControlSource changes dynamically, thealignment will be wrong. However, the following code will cause the column to re-evaluate thealignment:

WITH ThisForm.grdMyGrid.Column1 .ControlSource = "MyTable.cOtherCol" .Alignment = .AlignmentENDWITH

We've run into this problem on forms where we determine the RecordSource of the grid in theInit() of the form.

Mark A. Nadig is product development technical manager at Wind2 Software Inc., a Fort Collins,Colorado, publisher of accounting and business management software. 970-482-7145, CompuServe74250,1500.

An Error Handling Class for Visual FoxPro Doug Hennig (4)

This month's column presents a error handling class you can use to learn about error handling inVisual FoxPro. It can also serve as the foundation fora robust error handler to use in yourapplications.

Last month, I discussed error handling in Visual FoxPro. I looked at how VFP's error handlingdiffers from FoxPro 2.x and explored some strategies for dealing with different error situations.

This month, I'll look at an error manager class. This class isn't the pinnacle (no pun intended) oferror handlers, but rather demonstrates error handling techniques in VFP and serves as thefoundation for a more robust error handler.

Several ideas in this column come courtesy of Tom Rettig, one of the brightest lights in theFoxPro community and one of the nicest people I've ever met. Tom passed away earlier thisyear, and the vacuum he left behind won't easily be filled.

The ErrorMgr class

I'll start by looking at the definition of the ErrorMgr class, then I'll look at some examples ofhow it's instantiated and how it provides error handling services to an application.

ErrorMgr is a non-visual class based on the Custom base class. It's included in download file_HENNIG.EXE in the MANAGERS.VCX visual class library.

ErrorMgr uses the ERROR.H include file for the definitions of several constants used bymethods of the class:

#include '\VFP\FOXPRO.H'#define ccNULL chr(0)#define ccCR chr(13)#define ccQST_DEBUG 'Display the debugger?'

* AERROR() array dimensions, including added* extensions.

#define cnVF_AERR_MAX 7#define cnVF_AERR_NUMBER 1#define cnVF_AERR_MESSAGE 2#define cnVF_AERR_OBJECT 3#define cnVF_AERR_WORKAREA 4#define cnVF_AERR_TRIGGER 5#define cnVF_AERR_EXTRA1 6#define cnVF_AERR_EXTRA2 7#define cnAERR_MAX cnVF_AERR_MAX + 3#define cnAERR_METHOD cnVF_AERR_MAX + 1#define cnAERR_LINE cnVF_AERR_MAX + 2#define cnAERR_SOURCE cnVF_AERR_MAX + 3

The ErrorMgr class has the following custom properties:

Name Status Initial Value Purpose cCurrError Protected "" Holds the former ON ERROR handler. lSuppressErrors Protected .F. If .T., an error message isn't displayed when an

erroroccurs. lErrorOccurred Protected .F. Set to .T. when an error occurs. cTitle Public "Error" The title for the error message dialog box. aErrorInfo[1] Public "" An array containing information about the last

error.

Let's take a look at the Init method for the class. It accepts a parameter representing the title touse for the dialog box displayed when an error occurs, stored in the cTitle property. If you don'tpass the title when you instantiate the class, you can still set the title by changing this property.The Init method saves the current error handler in its protected cCurrError property, initializesthe protected lErrorOccurred and lSuppressErrors properties to .F., and points VFP's errorhandler to the ErrorHandler method of the class:

* Save the existing ON ERROR setting. Set our* message box title if one is passed.

lparameters tcTitleThis.cCurrError = on('ERROR')if type('tcTitle') = 'C'This.cTitle = tcTitleendif type('tcTitle') = 'C'

* Point the ON ERROR method to our ErrorHandler * routine. Set flags to default values.

on error goError.ErrorHandler(error(), sys(16), ;lineno())This.lErrorOccurred = .F.This.lSuppressErrors = .F.

As is usually the case, the Destroy method cleans up things the class has changed; in this case, itresets VFP's error handler to the one that was in effect before the object was instantiated. Thereason for storing the cCurrError property to a memory variable before macro expanding it isthat you can't macro expand an object's property directly:

local lcErrorlcError = This.cCurrErroron error &lcError

Before we look at the ErrorHandler method, which does most of the work of the class, let's lookat a few other useful methods of the class. DidErrorOccur is an exposed method that returns thevalue of the lErrorOccurred property, which is protected so it can't be inadvertently changedoutside the class. This allows your program to determine if an error occurred:

return This.lErrorOccurred

ResetError is an exposed method that sets the value of the protected lErrorOccurred property to.F. and clears the aErrorInfo array. Use this method before executing some code likely to causean error (such as trying to open a table exclusively) to ensure the lErrorOccurred flag isn't .T.(which it may be from a previous error), which would falsely lead you to believe an erroroccurred:

This.lErrorOccurred = .F.dimension This.aErrorInfo[cnAERR_MAX]This.aErrorInfo = ''

The SetSuppressErrors method sets the protected lSuppressErrors property to the value passed to

the method and returns its former value. If the lSuppressErrors property is .T., the ErrorHandlermethod won't display an error message to the user. This allows you to handle certain types oferrors yourself:

lparameters tlSuppresslocal llSuppressllSuppress = This.lSuppressErrorsThis.lSuppressErrors = iif(type('tlSuppress') = 'L' ;and not isnull('tlSuppress'), tlSuppress, ;This.lSuppressErrors)return llSuppress

Here's an example of using these three methods. Suppose you want to open a table exclusively,but if you can't, you don't want the user to see a generic error message; instead, you want todisplay your own message. The SetSuppressErrors method tells the error manager to suppresserrors for now, the ResetError method ensures the error flag is cleared, and the DidErrorOccurmethod tells you if an error occurred trying to open the table:

llSuppress = goError.SetSuppressErrors(.T.)goError.ResetError()use CUSTOMER exclusivegoError.SetSuppressErrors(llSuppress)if goError.DidErrorOccur()= messagebox('The customer table cannot be ' + ;'opened exclusively at this time.')returnendif goError.DidErrorOccur()

Now, let's look at the ErrorHandler method, which acts as the VFP error handler as long as theobject exists. When an error occurs, three parameters are passed to ErrorHandler: ERROR(), theerror number; SYS(16), the name of the program in which the error occurred, and LINENO(),the line number where the error occurred. ErrorHandler uses the GetErrorInfo method to set thelErrorOccurred property to .T. and puts information about the error into the aErrorInfo property.If the lSuppressErrors property is .T., no error message is displayed. Otherwise, the DisplayErrormethod displays a message about the error and gets the user's choice about what action to take.The choices are display the debugger, which brings up the Trace and Debug windows; don'tdisplay the debugger, which simply causes the program to carry on; or Cancel, in which caseErrorHandler shuts down the application:

lparameters tnError, tcMethod, tnLinelocal lcCurrTalk, lnChoice

* Ensure TALK is off.

if set('TALK') = 'ON'set talk offlcCurrTalk = 'ON'elselcCurrTalk = 'OFF'endif set('TALK') = 'ON'

* Put the error into the aErrorInfo array and* set the lErrorOccurred flag.

This.GetErrorInfo(tcMethod, tnLine)

* If errors aren't being suppressed, display the* error and get the user's choice of action.

if not This.lSuppressErrorslnChoice = This.DisplayError()do case

* Cancel: remove any WAIT window, issue a CLEAR* EVENTS, and then cancel.

case lnChoice = IDCANCELwait clearclear eventscancel

* Display the debugger: activate the Trace and* Debug windows, and keyboard an "O" to get out* of this routine and back to the one that* called us.

case lnChoice = IDYESactivate window debugkeyboard 'O' plainset step onendcaseendif not This.lSuppressErrors

* Restore TALK.

if lcCurrTalk = 'ON'set talk onendif lcCurrTalk = 'ON'

The GetErrorInfo method sets the lErrorOccurred property to .T. and puts information about theerror into the aErrorInfo array:

lparameters tcMethod, ;tnLine= aerror(This.aErrorInfo)dimension This.aErrorInfo[cnAERR_MAX]This.aErrorInfo[cnAERR_METHOD] = tcMethodThis.aErrorInfo[cnAERR_LINE] = tnLineThis.aErrorInfo[cnAERR_SOURCE] = message(1)This.lErrorOccurred = .T.

Thanks to the late Tom Rettig for the idea of using the AERROR() function to fill out most ofthe information, then expanding the array to include additional information left out byAERROR(). Here's the structure of the aErrorInfo array:

Element # Contents 1-7 Same as AERROR() 8 The method or procedure the error occurred in9 The line number the error occurred on 10 The line number the error occurred on

DisplayError displays a dialog box showing information about what the error was and where itoccurred and gives the user the choice of displaying the Trace and Debug windows, continuingon with the program, or canceling the application:

local lcLine1, lcLine2, lcLine3, lcLine4, ;lnPos, lcModule, lcObject, lcMethod, lcLine5, ;lcLine6, lcLine7lcLine1 = 'Error #: ' + ;ltrim(str(This.aErrorInfo[cnVF_AERR_NUMBER]))lcLine2 = 'Message: ' + ;This.aErrorInfo[cnVF_AERR_MESSAGE]lcLine3 = 'Line: ' + ;ltrim(str(This.aErrorInfo[cnAERR_LINE]))lcLine4 = 'Code: ' + ;This.aErrorInfo[cnAERR_SOURCE]

* Figure out the object, method, and module.

lnPos = rat(' ', This.aErrorInfo[cnAERR_METHOD])lcModule = substr(This.aErrorInfo[cnAERR_METHOD], ;lnPos + 1)lcModule = substr(lcModule, rat('\', lcModule) + 1)lcModule = left(lcModule, at('.', lcModule) - 1)lcObject = left(This.aErrorInfo[cnAERR_METHOD], ;lnPos - 1)lcObject = substr(lcObject, at(' ', lcObject) + 1)lnPos = rat('.', lcObject)lcMethod = substr(lcObject, lnPos + 1)lcObject = left(lcObject, lnPos - 1)lcLine5 = 'Method: ' + lcMethodlcLine6 = 'Object: ' + lcObjectlcLine7 = 'Module: ' + lcModule

* Display the messagebox and return the user's choice.

return messagebox(lcLine1 + ccCR + lcLine2 + ;ccCR + lcLine3 + ccCR + lcLine4 + ccCR + ;lcLine5 + ccCR + lcLine6 + ccCR + lcLine7 + ;ccCR + ccCR + ccQST_DEBUG, MB_YESNOCANCEL + ;MB_ICONSTOP, This.cTitle)

Using ErrorMgr

ErrorMgr expects to be instantiated into a global variable called goError. goError should bedeclared Private rather than Local so error trapping is available globally. If you'd rather not storethe reference to the ErrorMgr object in a global variable, you could store the reference in aproperty of an application-wide object, such as an application class:

oApp = createobject('Application')oApp.oError = createobject('ErrorMgr')

The Init method of ErrorMgr would have to be changed to set ON ERROR tooApp.oError.ErrorHandler instead of goError.ErrorHandler.

Here's a program (TESTERR.PRG in download file _HENNIG.EXE) that demonstratesinstantiating ErrorMgr, how it handles errors, and how the error message can be suppressed anderrors detected locally:

* Instantiate the ErrorMgr class so we have error * trapping. goError is Private rather than Local* so error trapping is available globally.

private goErrorset classlib to MANAGERSgoError = createobject('ErrorMgr', ;'My Error Title')

* Let's do something stupid to see if the error* handler works.

? sfsff&& this will cause an error

* Let's suppress error trapping so we can handle* it ourselves.

llSuppress = goError.SetSuppressErrors(.T.)goError.ResetError()? sfsff&& this will cause an errorgoError.SetSuppressErrors(llSuppress)if goError.DidErrorOccur()wait window 'The following error ' + ;'occurred: ' + goError.aErrorInfo[2]endif goError.DidErrorOccur()

If you want an object to handle certain errors itself but delegate other errors to ErrorMgr, usecode similar to the following in the object's Error method:

LPARAMETERS nError, cMethod, nLineif nError = <error number we can handle>* handle the errorelsegoError.ErrorHandler(nError, cMethod, nLine)endif

Expanding ErrorMgr

You've probably noticed that ErrorMgr is about as bare-bones as error handlers come. It simplydisplays an error message and gives the user a few options about what to do about it. That's whyI said at the beginning of this article that this would be the foundation for an error handler ratherthan one you'd use in a robust application. Let's look at some of the ways you'd want to enhanceit.

Although having an option to display the Trace and Debug windows is handy for a developer, it's

not a good idea for the typical user, and those features aren't available in a distributed .EXE.You'll need some way to turn off this choice under those conditions.

You'll probably want to support other choices for action as well. Retry is an obvious addition.Returning to the main program, although tricky to implement (see last month's column for someideas), might be useful as well.

You'll likely want to add the ability to handle different types of errors in different ways. Forexample, if the error is caused because the printer is off-line, you'd want to inform the user andgive them a chance to correct the problem and try again. If a record is locked, you'd also want togive them the option of trying again. In the case of a serious problem (such as low memory orsomething being corrupted), you might not want to give the user any option other than to exit theapplication. As I mentioned in last month's column, one way to deal with this diversity is tocreate a table containing error numbers, the message to display, and possibly information aboutwhat action to take. You'd change the DisplayError method to look up the error number in thistable, display the appropriate message, and present the user with options particular to the error.You'd also have to change ErrorHandler to handle more possible choices the user could selectfrom.

An elegant way of displaying messages to the user comes courtesy of Tom Rettig. In his TRUElibrary (available on CompuServe), Tom defined error messages as constants in an INCLUDEfile. These messages included information about where to insert custom information. Forexample, the error message for "database not open" might be as follows:

#define ccERR_DB_NOT_OPEN'The database <Insert1> is not open.'

He also defined a constant as the "place holder" string (there were actually several of these, incase a message needed more than one place holder):

#define ccMSG_INSERT1 '<Insert1>'

To display a customized error message, he used the STRTRAN() function to replace the placeholder with the actual information. Here's an example:

wait window strtran(ccERR_DB_NOT_OPEN, ; ccMSG_INSERT1, 'MYDATA')

You could use a technique similar to this to store messages in the error message table, andcustomize the message before displaying it to the user.

If you want to log the error to an error log file (I explored some strategies about error log fileslast month), you should create a new method (perhaps LogError) and change ErrorHandler to

call it before checking if lSuppressErrors is .F.; that way, even if an error message isn't displayedto the user, the error is still logged.

Conclusion

The last two columns have explored error handling in VFP. You've seen that while VFP givesyou more options for handling errors than FoxPro 2.x, that flexibility comes at the price of morecomplexity. An error handling class such as ErrorMgr can help deal with the complexity and canbe expanded or subclassed to provide more robust error handling services.

Next month, I'll discuss how new features in Windows 95 affect FoxPro applications and look atsome strategies for creating cross-platform (Window 3.1 and Windows 95) objects and code.

Doug Hennig is a partner with Stonefield Systems Group Inc. in Regina, Saskatchewan, Canada. He isthe author of Stonefield's add-on tools for FoxPro developers, including Stonefield Data Dictionary forFoxPro 2.x and Stonefield Database Toolkit for Visual FoxPro. He is also the author of The VisualFoxPro Data Dictionary in Pinnacle Publishing's The Pros Talk Visual FoxPro series. Doug has spokenat user groups and regional conferences throughout North America. CompuServe 75156,2326.

ProMatrix 1.1: Is It Right for You? Rod Sparkman

No one can deny that Visual FoxPro is the now of programming. However, your organization maynot be ready to move to OOP because of political or economic constraints. Let's take a look atProMatrix, a third-party application generator that can make short work of FoxPro for Windows2.x applications.

By finding third-party products that enhance an already robust development environment, youleverage your investment in FoxPro 2.x until you're ready to shift to OOP. A late-comer to theFoxPro for Windows environment, ProMatrix 1.1 (by Lawson-Smith Corp.) is such a product,providing many features to augment FoxPro.

Documentation

The documentation is clear and easy to read. The people at Lawson-Smith obviously have hadexperience writing technical manuals. They have organized the manual into logical sections thatare easy to reference. However, I recommend reading the manual thoroughly, since part of theapproach that ProMatrix uses isn't completely intuitive, and you'll find many valuable tips.

Installation

Installing ProMatrix wasn't without incident. Despite reading the warning, I failed to remove the

SET statements from my AUTOEXEC.BAT that pointed to libraries in my FoxPro subdirectory,resulting in ProMatrix crashing. When I followed the warning the second time, the programsinstalled without further problems. Although it was my error, I feel Lawson-Smith couldimprove the installation process in this area to make it easier, especially for novices.

If you follow the instructions explicitly, you can have the product up and running within a fewminutes. I advise that you install the sample application, as it's a good example of what a typicalProMatrix application looks like, and what types of functionality are possible during thedevelopment of your framework.

Features

ProMatrix offers several powerful features that I particularly like. These include a sophisticated,modeless event handler, a truly integrated data dictionary, a complete and tested applicationframework that will save you countless hours, and a migration path to Visual FoxPro 3.0.

ProMatrix's event-driven model is superior to any that I've used. I was able to layer screen afterscreen after screen populated with data. This is no simple task if you've ever attempted to do thiswith any success on your own in FoxPro. The task is even more difficult when you attempt tomaintain positioning of the records within the databases as you move from screen to screen.ProMatrix does this very nicely.

The worth of a product like ProMatrix can be measured by the integration of the data dictionaryinto the application. ProMatrix uses several FoxPro databases to keep track of those that you usein your application. Creating or adding a database to the dictionary is very easy. You simplyfollow the dialog boxes presented and enter the appropriate information. Afterwards, ProMatrixuses the native FoxPro structure dialog boxes to complete the process. I highly recommendcreating your databases in native FoxPro, and then adding them to the data dictionary later,which is very easy. If changes are needed, ProMatrix logically guides you through them.

Using intuitive dialog boxes, you may choose to create the data integrity rules for a specificfield. Some of those rules consist of when and how a field is to be displayed, who is to haveaccess, what pop-up list is associated with a field, and so forth. Context-sensitive field-level helpis built into the dictionary, and you may control access to a specific user to change the help fromwithin the distributed application. Building and maintaining indexes and databases is completelydialog box driven and is very easy. Because the data dictionary is the heart of the wholeapplication, I advise you to spend a lot of time here. Doing this will simplify screen and reportcreation.

The power of ProMatrix stems from its use of templates, complete with source code, and itsbuilt-in "wizard." For example, when creating a screen, the wizard allows you to select the fieldsyou want to display from the database dictionary. ProMatrix's wizard embeds source code fromthe base templates into the proper code snippets. Of course, you can modify the source codefrom the base templates since Lawson-Smith wrote them in native FoxPro. However, since thecode is rather sophisticated, I don't recommend that new or inexperienced users attempt it.

Generally, however, you should plan to modify the screens and their snippets that result from the

wizard. ProMatrix provides you with several examples of how to do it. I've taken a look at thecode in the templates, and it's well written and easy to understand. The source code has amplecomments to enhance readability. Please note that ProMatrix uses macro substitutionextensively. If you're unfamiliar with this technique, you'll want to brush up on it before doingany serious work.

The concept of an application framework isn't new. The idea is to have at your disposal a set ofthoroughly tested and re-usable modules integrated into the data dictionary to augment yourdevelopment. ProMatrix does this by providing you with several modules such as a reportmanager that allow you to develop, maintain, and run reports and labels. Writing this modulealone would take a good programmer several weeks or months to write.

Security within ProMatrix is extremely powerful. Within a few clicks of my mouse, I canactivate or de-activate logon access. In addition, I can set up groups with specific rights tomenus, screens, and fields. One feature that is particularly useful to me is the ability to display ornot display a field or button. This is very helpful when you want to use the same screen for twotypes of users but need to limit the display of certain fields to a specific group.

ProMatrix has built-in error handling. Any standard FoxPro error is routed to the error routinesand reported to the user. The error is then written out to a database that can be viewedcompletely with the memory contents of all variables. This feature will come to your aid whiledebugging your programs.

You can establish audit trails on field-level or overall system accesses. Field-level audit trails areset up through the data dictionary on a field by field basis. System-level audit trails are set up viathe administrative tools. If this feature is turned off, then field-level accesses will be turned off aswell. In like fashion, activity tracking can be turned on or off at will.

Lawson-Smith Inc. provides a migration path to "Visual" ProMatrix (which is still in betaaccording to company officials that I spoke to in early April) for use with FoxPro 3.0. Thismeans that when you create an application in ProMatrix 1.1, they'll provide you with the tools tomigrate that code to Visual FoxPro and the new ProMatrix. You may not have to abandon theinvestment you've made in ProMatrix 1.1. According to officials at Lawson-Smith, the delay inthe delivery of their Visual ProMatrix is due to "bugs" found in Visual FoxPro. This hasapparently impeded releasing a stable product.

Concerns

As with any complex product, ProMatrix does have some areas that are of concern to me. Theseinclude a lack of speed and the resulting need for a Pentium-class machine, lack of support forGENSCRNX, development for the Windows 3.x environment only and occasional failure of theerror routines to handles crashes.

A significant complaint of mine is lack of speed. In some areas, particularly in the file openingroutines, ProMatrix doesn't take advantage of SQL-SELECT statements using insteadSCAN/FOR statements. I've rewritten those routines to enhance the speed of my applicationresulting in a 50 percent improvement. All it required was a few minutes. Lawson-Smith needs

to look at this area particularly since optimization of the code will definitely provide muchsmoother performance.

I advise a Pentium-class machine for development if you expect to enhance your productivity.Compiles do take a while to complete. Manipulation through the wizard can be a bit slow andcumbersome for the advanced user of FoxPro. I've often bypassed ProMatrix opting instead touse native FoxPro when it came to making minor changes to the screens or databases. I thenreturn to ProMatrix to do the compile.

ProMatrix was created solely for use in FoxPro 2.6 for Windows. One of the competitiveadvantages of the FoxPro 2.x product line was the fact that they were multi-platform capable.You can transport your ProMatrix application to DOS, but it will require a significant effort onyour part since many screens created by Lawson-Smith will have to be changed to work withinthe DOS environment.

In certain circumstances, the error routines fail and the application crashes. Rather than logicallyexiting to the ProMatrix environment, the application will display the error message and then donothing. If you click on a menu item, then the native FoxPro error messages appear with theensuing ProMatrix code being displayed. This is especially troublesome requiring you to pressthe Escape key until the messages disappear. You then need to issue the command SETSYSMENU TO DEFAULT and re-execute the application. The program doesn't return toProMatrix but will exit to Windows. As nice as it is, I wish the error handler was more robust.

Conclusions

ProMatrix requires a short learning curve while affording the developer substantial gains inreduced development time. Is ProMatrix right for you? If you're unable to make the move toVisual FoxPro, need a jump start in Xbase with an entire application framework that is easy towork with, don't want to re-create the wheel, require a migration path to Visual FoxPro 3.0, andhave the $395 to buy the product, then ProMatrix may be in your future. You might impress yourmanager, some users, or even your colleagues with your FoxPro "expertise"!

Rod Sparkman is president of SparTechs Co., a software development and consulting firm, and is aFoxPro developer for the Church of Jesus Christ of Latter-Day Saints. Rod has experience in micro, mini,and mainframe systems. 801-240-5967, CompuServe, 102670,723.

Sidebar: ProMatrix 1.1

Lawson-Smith Corp.

5225 Ehrlich Road, Suite C

Tampa Florida 33624

800-889-7058

813-960-5882

Fax 813-960-7231

Create Context Menus in FoxPro 2.x/Mac Bill Anderson (5)

As the saying goes, real programmers don't need two buttons on their mouse, so that's why they useMacs. But how can a Mac developer take advantage of the context menus currently in vogue whenthere's no right mouse button to use? Bill Anderson solves the problem with a GENSCRNX driver.

This article is a follow up to an article by Rick Strahl ("Create Context Menus in FoxPro 2.x," inthe October 1995 Foxtalk). The article demonstrates some effective methods for using the right(or secondary) mouse button for displaying context menus.

So what's the problem? There is no secondary mouse button on the Macintosh -- there's only onebutton. FoxPro/Mac doesn't support ON KEY LABEL RIGHTMOUSE. Using parts of Rick'sroutine, I've been able to construct a GENSCRNX driver, CLICKSCR, that uses the left (orprimary) mouse button to display context menus. As a bonus, this driver works on any 2.xplatform.

How CLICKSCR works

The idea behind the CLICKSCR driver is rather simple. The driver adds a box to the screen asbig as the screen will allow and adds a *:CLICK function in the comment snippet. That's allthere is to it! The added box lies underneath the Get objects on the screen and therefore doesn'tinterfere with any mouse activities within them.

Like most GENSCRNX drivers, CLICKSCR uses the fifth driver hook. This directive should beloaded after all other *:SCXDRV5 directives. The driver also supports a directive(*:CLICKPRG) that is used as the primary mouse click processor.

Using CLICKSCR to display pop-up windows

The CLICKSCR driver is rather generic and requires a fair amount of programmer effort. Here'show I use CLICKSCR. First, add the following to your set of GENSCRNX Screen snippets:

*:SCXDRV5 CLICKSCR*:CLICKPRG Clkhandl(m.mwindow)

Then, at the bottom of your Setup screen snippet add the following:

PRIVATE mwindow#:SECTION 3m.mwindow = WinParent([{{UPPER(m.name)}}])= BldPopup(m.mwindow)

The memory variable m.mwindow is used to hold the return value from the WinParent function.The function WinParent simply returns the outermost window from the window call stack. Thiswindow name is passed to the BldPopup program. This program simply defines the pop-upmenus within this outermost window. When the screen is clicked, the ClkHandl program doessome housekeeping, moves the pop-up window to the click coordinates and displays it. Note thatthe programs WinParent, BldPopup, and ClkHandl must be in the calling stack at runtime.

A demonstration screen has been supplied in download file _ANDERSN.EXE. [Thanks to JacciAdams for independently trying out advance copies of the driver on her Mac. -- Ed.] My usershave found this pop-up display to be very convenient; I hope your clients do as well.

Bill Anderson is an independent consultant based in Southern California, specializing in cross-platformapplications. He's the author of 3DBOX.PRG and vice president of the L. A. Fox Users Group.213-851-7725, CompuServe 72712,3417.

Uncommitted Changes in Empty Tables,Returning Arrays, and VFP "Read Timeout" John V. PetersenI have a form where upon entry, I assign the value ofa bound TextBox Control to a predefined value. Thetable that this form acts upon currently has zerorecords. When I close the form, I get an error that thetable had uncommitted changes. I didn't pressed theadd button and didn't make any changes via thekeyboard. Why am I experiencing this behavior?

I've worked with the scenario you describe and have found that when a table or view has zerorecords and the Value property of a bound control is altered, a new record is appended. This iscausing the uncommitted changes error when you exit the form. It's certainly behavior that Iwouldn't have expected. Rather, I would have guessed that the value assignment would havebeen ignored. In any case, it's something you need to deal with, so let's explore the issue in alittle more detail.

First, it's important to understand that anytime you alter the Value Property of a bound control,

it's just as if you opened the table or view and manipulated the data in a Browse. In the case ofzero records, Visual FoxPro needs a record to manipulate. In the absence of one, Visual FoxProwill create a record. The type of locking scheme involved will determine how this behavior willmanifest itself.

• Pessimistic: Anytime you add records with pessimistic locking, VFP needs to be sure thatthe record(s) can be added. Therefore, a header lock will be in effect until you issue aTABLEUPDATE() or a TABLEREVERT(). As long as this header lock is in effect, otherusers can't add records.

• Optimistic: In the case of optimistic locking the record is added to the buffer and noheader lock exists. Maybe for this one reason alone, optimistic is preferred to pessimisticlocking.

In any case, it's important to understand that when zero records exist and the Value property of abound control is altered, VFP is going to add a record. It's also important to account for thepossibility of a new record even if your user doesn't press an add button.

In some programs I may use some routine to see if afunction is available. The function CONTINUE isbasically disabled and is only enabled after aLOCATE. Is there any programming function tocheck if the command (CONTINUE) is enabled ordisabled ?

Heinz-Dieter. Koschnicke, Analyst Programmer.

Brussels, Belgium

I assume you're referring to the Continue Bar of the Record menu. You can use the SKPBAR()function to determine if the CONTINUE bar is enabled or disabled. The SKPBAR() functionaccepts two arguments. The first is a character string denoting the name of the menu in which thebar resides. In this case, the internal name of the Record Menu is _mrecord. The secondargument is a numeric value denoting the bar number to test for whether it's enabled or disabled.The Continue Bar of the Record menu has an internal bar number of _mrc_cont.

IF SKPBAR("_mrecord",_mrc_cont) returns .F., it means the Continue Bar is enabled. If itreturns .T., it means the Continue Bar is disabled.

I use an OptionGroup to display informationregarding whether an individual is male or female. Inthe table, I store either "M" or "F." If the sex isn'tknown, the field contains .NULL.. Unfortunately,unless I store the entire word, "Male" or "Female,"or make the caption one character long, "M" or "F,"I can't display the information. I'd like to be able tostore one character and properly display either"Male" or "Female." How can I accomplish this?

The key is to make the OptionGroup an unbound control. This means you'll set theControlSource Property to an empty string. By taking advantage of a few methods and events,you can get all of the flexibility you need. Assuming the name of the name of the field is SEXand the name of the alias is CUSTOMER, you can use the following code:

PROCEDURE OptionGroup.Init() *Initialize to character THIS.Value = SPACE(0) *Make sure this control does not get bound THIS.ControlSource = SPACE(0)ENDPROC

PROCEDURE OptionGroup.Refresh() DO CASE CASE ISNULL(customer.sex) THIS.Value = SPACE(0) CASE UPPER(customer.sex) = "M" THIS.Value = "Male" CASE UPPER(customer.sex) = "F" THIS.Value = "Female" ENDCASEENDPROC

PROCEDURE OptionGroup.InterActiveChange() ** Because the field is only 1 character long, ** only "M" or "F" will be written. REPLACE customer.sex WITH THIS.ValueENDPROC

I'm passing an array to a form, allowing theuser to manipulate the array, and I now wantto return the array to the calling program. Ican't get the array to reflect the user'schanges once back in the calling program.What's the best method for having the callingprogram have access to the modified array?

Two answers come to mind in regard to this issue. The first involves using a private memoryvariable array in conjunction with a wrapper .PRG to call the form. I know what you're going tosay, "That's not OOP!" To which I reply, "Yes, but it gets the job done and sometimes we needto break the rules." The following is a skeleton of a wrapper .PRG to call our form:

PRIVATE paArrayDIMENSION paArray[1]DO FORM getarray && this form is modalDISPLAY MEMORY LIKE paArray && see our results

Because the array has been declared private, it's available to all aspects of the form since theform call is subordinate to this calling program. To illustrate the point further, assume that youronly task was to copy the contents of an array property of the form to my private array. You usedthe customer table in the Tasmanian Traders sample that comes with VFP. In the Init() of theGetArray form, you have the following code:

SELECT company_name ; FROM customer ; INTO ARRAY THIS.iaFormArray

In the Unload() Event of the GetArray Form, you have the following code:

DIMENSION paArray[ALEN(THIS.iaFormArray,1)]=ACOPY(THIS.iaFormArray,paArray)

Now for solution number two.

Most likely, you'll want a special form dedicated to handling the task of allowing a user to pickfrom a group of items, and returning only those items that he or she picked. Typically, you callthese mover box forms because the form usually consists of a source list on the left and a list ofselected items on the right. I've uploaded such a form to the public domain. You can find it in theHelpful Utilities Library in the VFOX Forum on CompuServe.

In FoxPro 2.x, I could use the TIMEOUTclause of the READ to automaticallyterminate a form after a specified period oftime lapsed without keystrokes. It appearsthat this capability is lost in VFP since wedon't use READs. How can I regain thisfeature?

This is a relatively simple task. It involves the use of the Timer Control and a few Events in theForm Control. After placing a Timer Control on the form, set the Interval Property to 300,000.Remember that the Interval Property works in units of milliseconds. There are 300,000milliseconds in five minutes. In effect, you're going to create a form that will terminate after fiveconsecutive minutes in which the user has not pressed a key. Next, you need to enter some codein the Timer's Timer() Event:

**Code to revert uncommitted changesTHISFORM.Release()

Next, you need to shift your focus to the form itself. First, set the KeyPreview Property to .T..Then, place the following code in the form's KeyPress() Event:

THIS.Timer1.Reset()

By invoking the Reset() method of the form's timer, you're telling it to start over again from zero.As you can see, as long as the user presses a key, the timer's Reset() method will be called. If theReset() method is continually called, the timer's Timer() Event will never fire. Thus, your formwon't be released until either you explicitly close the form or five consecutive minutes haveelapsed without a keystroke. By setting the KeyPreview Property of the form to .T., even ifyou're entering keystrokes to a control on the form, the form's KeyPress() Event will still fireresetting the timer.

John V. Petersen, MBA, is director of FoxPro and Visual FoxPro Development and Training for PearlComputer Systems Inc., a Microsoft Solution Provider and authorized SBT Reseller based in MountLaurel, New Jersey. 609-983-9265. John is active in the FoxPro community, has written for publicationsin the United States and has been a speaker at user group meetings and at the 1995 Developer DaysConference. CompuServe 103360,1031.

Setting a Good Example

Les PinterI'm going to give some of my friends at Microsoft a hard time, and I want to apologize inadvance. I do so with considerable regret, because they're nice folks, and it's not entirely theirfault. But this is a topic that needs to be dealt with.

It's the documentation that comes with FoxPro. Who ever saw reference manuals that couldsatisfy everyone (if not to say anyone). And if it were all we ever needed, my six FoxPro bookswouldn't have found a market. But I think one topic in particular poses special problems. Itinvolves explaining object-oriented programming using analogies.

The manuals that come with FoxPro describe inheritance using a telephone metaphor. Use thetelephone class to make another telephone, and your new telephone inherits the characteristics ofthe telephone class.

I understand the metaphor, but it doesn't tell me how to write and use classes. Alan Schwartzwrote a terrific article about this topic. You have a ball. It has a color property. You set the colorproperty to red. Now you have a red ball. What the hell does that have to do with programming?Analogies about the birds and the bees have left children bewildered for years, andobject-oriented programming articles that don't explain what really happens during coding aredestined for the same fate.

It's not hard to describe a deficiency in a FoxPro base class (I use the difficulty of finding thecursor in input fields that are all the same color) and add method code to fix the problem (likechanging ForeColor and BackColor in the GotFocus and LostFocus methods). Changing the dullgray of the default SelectedItemForeColor and SelectedItemBackColor properties of comboboxes and list boxes is another. These are easy to explain, and immediately demonstrate howinheritance works. We're technical people, and we're not stupid.

I once spent half an hour listening to a very bright fellow talk about how to write classes inFoxPro, and left without having learned a thing. I was in college full time for 11 years. I havethree degrees. When I don't understand, I don't assume the fault lies with me. Especially whenthe person explaining rather seems to enjoy the fact that I'm not following. Good examples areeasy to understand. Don't blame the victim.

So please, Microsoft, forego the analogies. Programming isn't like a red ball, and it isn't like atelephone. It's like programming. A few lines of code are worth a thousand pictures.

Les Pinter publishes the Pinter FoxPro Letter in the United States and Russia. 916-582-0595.

Tricks to Tame Your Forms Compiled by Barbara Peisch

Use Custom Properties to Extend Flexibility

Setting AutoCenter to .T. and BorderStyle to "2 - Double Wide Border" is the default behaviorthat most developers would prefer. However, these settings aren't the settings you want at designtime. If you set these properties in the Foundation class for your forms, this means that in theForm Designer the form is always centered on the _SCREEN (not centered within the formdesigner) and the border isn't resizable at design time. You could set AutoCenter = .T. andBorderStyle = 2 in the form Foundation classes Init() method, but then you have to override themethod to change this behavior in a form instance or subclass.

You can have the best of both worlds if you create two additional properties in your Foundationclass -- lAutoCenter and nBorderSztyle -- then have the following code in the Foundation classesInit():

This.AutoCenter = This.lAutoCenterThis.BorderStyle = This.nBorderStyle

Now you can set these two properties in an instance or a subclass, leaving AutoCenter andBorderStyle at their default settings, which are most convenient for design and have theappropriate settings at runtime. -- Steve Sawyer

Watch Out for Those Wizards

Wizards are great tools for creating a variety of objects on your forms, but they don't alwaysleave properties the way you want them. If you've ever canceled a wizard before it completed,you've probably discovered that the LockScreen property on the form was set to .T., therebyhiding all the objects on your form.

Even if you let the wizard run its course, make sure you check the LockScreen and Visibleproperties of your form and objects. Although the Wizard will restore the settings of theseproperties to their defaults, they will appear in bold. This means the settings of those propertiesare no longer inherited from the parent. To restore these properties so they inherit from theparent again, simply right click on the property and select "Reset to Default" from the menu thatappears. -- Barbara Peisch