------------------------------------------------------------------------------ ------------------------------------------------------------------------------ NNNNNNNNNNNN NNNNNNNNNNNN NNNNNNN NNNNNN NNNNNNN NNNNNN NNNNNNNNNNNNNN NNNNNNNNNNNNNN NNNNNNNN NNNNNN NNNNNNNN NNNNNN NNNNNN NNNNN NNNNNNNNNN NNNNNNNNN NNNNNN NNNNNNNNN NNNNNN NNNNNN NNNNN NNNNNNNN NNNNNNNNN NNNNNNN NNNNNNNNNN NNNNNN NNNNNNNNNNNN NNNNNN NNNNNNNNNNNNNNNNN NNNNNNNNNNNNNNNNN NNNNNN NNNNNN NNNNNNN NNNNNN NNNNNNNNNN NNNNNN NNNNNNNNNN NNNNNN NNNNNN NNNNNNNN NNNNNN NNNNNNNN NNNNNN NNNNNNNN NNNNNNNNNNNNNN NNNNNNNNNNNNN NNNNNN NNNNNNN NNNNNN NNNNNNN NNNNNNNNNNNNN NNNNNNNNNNN NNNNNN NNNNNN NNNNNN NNNNNN ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ Boston Computer News Network October 1994 A Service of the Boston Computer Society, USA Vol.1, No.7 Sponsored by the Foxpro SIG Foxpro Version ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ Table of Contents ----------------------------------------------------------------------- 0. Introduction. 1. User Group Meetings. 2. Developers Conference - Frankfurt. 3. Remind Me. 4. Shadow Table Techniques. 5. Data Integrity Tool. 6. Poor Man's FoxDoc. 7. Mouse Around with the 'Other' Mouse Button. 8. I thought I knew All the Tricks. 9. View your Diary. 10. Been Thinking 'bout The Pretender(s)... 11. BCNN Statement of Ownership, Copyright, and Responsibility. 0. Introduction. ----------------------------------------------------------------------- ReplyTo: David Rose [73164,2263] Wow! What a mammoth issue! Well, our readers said they wanted more code snippets, and this issue's just chock full! It's kind of like an early Thanksgiving (or whatever the holiday is called where you live with the great, big meal). 1. User Group Meetings. ----------------------------------------------------------------------- ReplyTo: Arnold Bilansky [71533,1031] (617)522-3700 x374 Place: Microsoft Office, 9 Hillside Avenue, Waltham, MA USA. Meeting: Nov. 9, 1994, 7:00 p.m. Darrell Weldon & Frank Weyant: "Selecting Between FP, VB & Access" Meeting: Nov. 30, 1994, 7:00 p.m. Jim Booth: "Screen Builder Tips & Tricks" Incestuous Meeting: Nov. 15, 1994, 7:00 p.m. "Client/Server for xBase Developers, Featuring PowerBuilder" Non-MS Meeting Site: Sheraton Tara Hotel, Lexington (Exit 2A, Rte 128) 2. Developers Conference - Frankfurt. --------------------------------------------------------------------- ReplyTo: Rainer Becker [dFPUG] [100024,1364] Fax: 0049-(0)6173-65997 Phone: 0049-(0)6173-68182 CIS: 100024,1364 FOXPRO - Developers Conference, 1st and 2nd December, 1994 Frankfurt/Main Airport, Germany Lisa C. Slater, Alan Schwartz, Steven Black, Calvin Hsia, Andrew Ross MacNeill and John L. Hawkins visit Europe! The German-speaking FoxPro User Group dFPUG invites you to the FoxPro Developers Conference at Frankfurt/Main Airport. 32 contributions (18 english sessions, 90 minutes each) on and about FoxPro are being offered. A dozen speakers from all over the world introduce ideas, approaches and practical strategies for advanced FoxPro users. Emphasis is put on subjects with a promising future and undiminished relevance under Visual FoxPro. Download the english conference program from the DFPUG-library in the MSCESYS-Forum (GO MSCESYS, library 16) or send your fax number via eMail to the CIS-ID 100024,1364. The FoxPro Developers Conference is being organized by the German- speaking FoxPro User Group dFPUG (deutschsprachige FoxPro User Group) with support of Microsoft, Ziff Messe & Konferenz GmbH and Data Communications (Data Based Advisor, FoxPro Advisor). To those who don't know the dFPUG yet: The dFPUG is a user group independent of software suppliers, consisting of more than 850 members. It has existed for two years and offers a wide range of services. You can order further information on the dFPUG including a free copy of its bimonthly newsletter and a complete program of the conference from the address given below. dFPUG / ISYS GmbH Messerschmittstr. 2 a D-65760 Frankfurt/Main Germany 3. Remind Me. ----------------------------------------------------------------------- ReplyTo: Ted Roche [76400,2503], Computer Resource (603) 746-5670 The FoxPro startup sequence is a nice place to store loads of neat stuff, as we've discussed in past issues - defining keyboard macros, popping up a colorful background, or SETting important environmental settings. What if you'd like to get reminded of an important upcoming date - someone's birthday, your anniversary, or DevCon? That's easy, too, just add the single line =DTD() to your startup program. You can check anytime to see how long you've got to wait by issuing the same command in the command window, if you've stored DTD.PRG along your PATH. * DTD - Days 'til DevCon wait window nowait ltrim(str({01/15/95}-date())) + ; " days 'til DevCon '95!" return 4. Shadow Table Techniques. ----------------------------------------------------------------------- ReplyTo: Steven Sawyer When utilizing indirect editing of table data in a FoxPro screen, the question of how to deal with records in a child table arises. The FoxPro BROWSE command is very well suited to dealing with child table records, and with the introduction of browse windows as screen objects with the release of FoxPro 3.0, this method will probably become more common. To be consistent with the principle behind indirect editing of table data, creation of a temporary table or cursor for creation or editing of child records using a BROWSE is a common practice. The developer, once they have decided to use this technique, is then faced with the problem of transferring this data back to the permanent table. Here is a technique I use that makes use of a generic procedure to actually update the permanent table according to the user's edits, and allows complete flexibility in terms of adding, deleting, or changing records. First, if the function is to edit existing records, create the temporary table using an SQL-SELECT, or, if creating new records, create a temporary table using CREATE TABLE or CREATE CURSOR as shown below. Note that in both cases, all child-table fields are selected/duplicated, and an additional field is added to hold the record number. When editing existing child records: SELECT *, RECNO() AS nrecord FROM detail WHERE detail.ino = 1078 When creating new child records: USE detail =AFIELDS(laDtlTmp) DIMENSION laDtlTmp[ALEN(laDtlTmp,1)+1,4] lnNewFld = ALEN(laDtlTmp,1) laDtlTmp[7,1] = "NRECORD" laDtlTmp[7,2] = "N" laDtlTmp[7,3] = 9 && Adjust as appropriate for * ** likely max record number laDtlTmp[7,4] = 0 CREATE TABLE FROM ARRAY laDtlTmp APPEND BLANK From this point you allow the user to make whatever changes are needed to the resulting temporary tables, adding records, deleting records changing field contents etc., all under the control of whatever validation routines you wish to use. When the user is finished, do whatever "cleanup" is necessary, such as deleting records that the user has "zeroed out", performing item extensions, initializing key field values etc. Once the temporary table is as you (and the user) wish the permanent table to appear, you call my PostBack procedure, with the alias of the temporary and permanent tables as parameters. POSTBACK adds records to the permanent table for records *not* having a record number field in the temporary table (i.e. these are *new* records), deletes records from the permanent table that are deleted in the temporary table, and updates the permanent table record-by-record to reflect the contents of the temporary table. If your practice is to recycle records, POSTBACK can be easily modified to "blank" records in the permanent table which correspond to deleted records in the temporary table, instead of deleting them. *--------*---------*---------*---------*--------- * Procedure.........: PostBack *) Description.......: When using a temporary table *) ..................: to create or edit child *) ..................: records, this procedure *) ..................: transfers the information from *) ..................: the temporary table to the *) ..................: permanent table. *) ..................: The temporary table must be created *) ..................: using an SQL-SELECT statement or a *) ..................: CREATE TABLE or CREATE CURSOR which *) ..................: selects or re-creates *all* of the *) ..................: fields, and creates one additional field *) ..................: to contain the record number of the *) ..................: record in the permanent table. *) ..................: When creating new records to add to the *) ..................: permanent table, the value of the record- *) ..................: number field is left as "0" *) ..................: Records deleted in the temporary table *) ..................: are deleted in the permanent table * Syntax............: DO PostBack WITH * ..................: , * ..................: (note both tables must be open when this * ..................: procedure is called) * Parameters........: pcTmpAlias, Character, alias of temp. table * ..................: pcPrmAlias, Character, alias of perm. table *--------*---------*---------*---------*--------- PROCEDURE PostBack PARAMETERS pcTmpAlias, pcPrmAlias PRIVATE ALL LIKE j* jcOldDel=SET("DELETED") SET DELETED OFF LOCATE SCAN * Is this an existing record i.e. * do we have a record number? IF EVALUATE(FIELD(FCOUNT())) > 0 * Note: The last record of the temporary table * MUST be the "record number" field! jnRecord = EVALUATE(FIELD(FCOUNT())) GOTO jnRecord IN (pcPrmAlias) IF ! DELETED() SCATTER TO jaTemp SELECT (pcPrmAlias) GATHER FROM jaTemp ELSE SELECT (pcPrmAlias) DELETE ENDIF SELECT (pcTmpAlias) ELSE IF ! DELETED() SCATTER TO jaTemp INSERT INTO (pcPrmAlias) FROM ARRAY jaTemp ENDIF ENDIF ENDSCAN SET DELETED &jcOldDel RETURN *EOP PostBack 5. Data Integrity Tool. ----------------------------------------------------------------------- ReplyTo: David Rose [73164,2263] Internet: d_rose@necx.com Library: CompuServe FOXUSER Lib 9 Download: DATATE We live in an imperfect world. While this should be no surprise to any of you, it certainly can be frustrating. I wrote this little tool as a way of automating the difficult and unrewarding task of locating inconsistencies in a database. Possible uses could include testing performance of an upgraded application, handling failures during transaction processing, or the felicity of mirrored databases. If you support an application that executes multi-table transactions in Foxpro, you know that a failure of hardware, software or the network during the execution of a transaction can leave your data in an inconsistent state. By building in a rules checker, you can better and more quickly clean up following such occurrences. The idea is simple, the application reads a list of rules which are encoded as executable Foxpro statements. There are functions provided to make these statements fairly easy, and include tests such as required child records, primary and alternate key uniqueness, and existence of primary keys to match foreign keys. The application has a facility to use tables that are stored in a second table, which permits one to modify the tests for different data sets with the same constraints. The application also is very careful to continue running even when the execution of a rule test causes a Foxpro error to occur. (It does this by using the old dBase "RETURN TO MASTER".) This means that the non-existence of an index or a record out of range error does not derail the entire test check, instead it simple reports that the test failed due to that error, and goes on to run the next test. While the application itself is too long to go into detail here, I want to show one of the programs used to specify a kind of integrity rule: function TSTFK * Test for a required parent/child relationship parameters tcParent, tcParKey, tcChild, tcChldTag, tcFor * tcParent = table alias of parent * tcParKey = field expression that is primary key * tcChild = table alias of child * tcChldTag = index tag of child, the expression is the foreign key * tcFor = only records in parent meeting condition are tested private lnCount, llReturn llReturn = .T. =TSTFILE(tcChild) && Program uses file dictionary to open table. set order to tag (tcChldTag) && Child tag is assumed =TSTFILE(tcParent) && Program uses file dictionary to open parent set relation to &tcParKey into (tcChild) * The idea here is to run through the parent records seeing if the * child table has no matching records. If child is at EOF(), then * a matching record could not be found. lnCount = 0 if empty(tcFor) tcFor = ".T." endif * I have found the inclusion of the tcFor parameter to be helpful * when tables are really mixed entities or there are criteria for * child records to exist that can be identified from the parent * table scan all for &tcFor if eof(tcChild) * No child found, report on this using textmerge to the log... \<> <> No child for key [<>]. llReturn = .F. lnCount = lnCount+1 * during testing, sometimes so many violations occur that there * is no point to listing them all, in this case gnCUTOFF stops * the rules test once that many exceptions have been found. if lnCount >= gnCUTOFF \<> <> etc. ... exit endif endif endscan set relation to use select (tcChild) use return llReturn That is it! Since this is a work in progress, I would expect that much could be done for the next version. If data integrity is something that you are serious about and this article has intrigued you, download the full application from Compuserve and try it out. I would gladly accept feedback on suggestions, improvements or problems. And I hope that this little tool can help you with your data integrity problems. 6. Poor man's FoxDoc. ----------------------------------------------------------------------- ReplyTo: Steven Black [76200,2110]. Ever wonder what programs call a particular module in a project? Can't bear to use or wait through FoxDoc? Try this: USE MyProject.PJX AGAIN SET FILTER TO "MYFUNC" $ SYMBOLS BROWSE The calling modules (and the program itself) can be reckoned by examining the NAME memo field in the resulting browse. Beware: Reports and Labels that reference "MyFunc" won't appear on this list. 7. Mouse Around with the 'Other' Mouse Button. ----------------------------------------------------------------------- Reply: Rick Strahl 76427,2363 [76427,2363] West Wind Technologies Phone: (503) 386-2087 Fax: (503) 386-7480 In case you haven't noticed: PC mice have more than one button. It seems like it took forever for software companies to figure this out and add any functionality for this forgotten button in their products. The most common way that you see the right mouse button used is in object inspector type menus in applications like Word and Excel. It's a jazzy, handy feature that you can easily implement in your FoxPro apps by attaching a routine to your RIGHTMOUSE key that pops up a popup at the current mouse location. Just for this purpose I've built a generic routine that takes the mundane work out of creating the menu popups, so that the menu creation and action commands can all be handled by a single call to this function directly from your ON KEY LABEL RIGHTMOUSE command. For example on a date field you might pop up the following: ON KEY LABEL RIGHTMOUSE ; DO QK_MENU WITH ; "Show \ lnMaxLen lnMaxLen = LEN(lcPromptVal) ENDIF ENDFOR &&* x=1 to lnParms *** Activate screen so we use DESKTOP for popup ACTIVATE SCREEN DEFINE POPUP qk_menu IN SCREEN MARGIN RELATIVE ; SHADOW COLOR SCHEME 4 FROM lnRow,lnCol *** Popup Prompts FOR x=1 TO lnParms DEFINE BAR x OF qk_menu PROMPT a_parms[x,1] ENDFOR &&* x=1 to lnParms ON SELECTION POPUP QK_MENU DEACTIVATE POPUP QK_MENU ACTIVATE POPUP QK_MENU RELEASE POPUP QK_MENU *** Escape or clicked outside of Popup IF BAR()>0 *** Execute the command of the option specified lcCommand=TRIM(a_parms[bar(),2]) IF !EMPTY(lcCommand) &lcCommand ENDIF ENDIF *** Reset Screen Font IF _WINDOWS MODI WINDOW SCREEN FONT (font_name),(font_size) ; STYLE (font_style) ENDIF RETURN BAR() *EOP Qk_Menu This menu routine works by passing up to 15 popup prompt and command pairs to QK_MENU. Any popup prompt can be passed including hotkeys, disabled options and separator bars. Every prompt must have a corresponding command - if you don't need a command (separators for example) simply pass a Null ("") string. By default the menu pops up at the current mouse cursor location, but you can also force the popup to center by adding a final parameter of "CENTER", or to a specific location by passing a pair of numeric coordinates for the row and column where you would like to pop up the menu. A few things of note happen inside this routine. First off you'll notice that there are ton of parameter variables to this routine. I could have used an array to pass to QK_MENU, but that would require the user to setup the array and fill it with the same values we now pass to QK_MENU - ease of use overruled form in this case. The prompt/command pairs are passed directly and, using a naming convention that includes a stem plus a 'parameter number', the arguments are parsed into an array by QK_MENU internally. You'll also notice that for the Windows version I'm setting the screen font to Arial, 9 point. Popups are an odd-ball feature in FoxPro for Windows in that you cannot set the font for them - generally the font used is the Windows default which is the System Font. However, if your current window font or the desktop font is FoxFont, the popup will come up in the FoxFont, which is quite ugly when used in a popup. Making sure that the screen font is Arial, and then explicitly issuing ACTIVATE SCREEN forces the popup to take on the more pleasing System font. Designing truly context sensitive applications is a tedious task. Even allowing the RIGHTMOUSE to pop up menus on just a few fields can be a lot of work. In order to further simplify implementing the RIGHTMOUSE functionality I've taken to using a global RIGHTMOUSE key handler. Using a single routine to handle all your RIGHTMOUSE related functionality frees you from having to worry about setting and resetting the ON KEY LABEL RIGHTMOUSE key all the time for single fields. You can simply activate your mouse handler once at the top of your app and have it work throughout, allowing you to use default assignments when no specific options are available for the current operation. For example, I can automatically attach the date routines that are shown in the example above to all of my date fields. All zipcode fields in my apps get the option to look up a Zipcode table, all numerics pop up a calculator. These 'default' operations can then be overridden by specific conditions that you specify in one large CASE statement. Here's my RMOUSE handler routine skeleton with a few sample options in it: **************************************************************** PROCEDURE RMouse ****************** *** Author: (c) Rick Strahl, 1994 *** Modified: 10/01/94 *** Function: Global RightMouse Handler activated whenever the *** right mouse key or Ctrl-Spacebars is activated. *** *** Assume: Application specific *** requires FoxPro 2.6 (OBJVAR()) *** *** Call with: ON KEY LABEL RIGHTMOUSE DO rmouse *** ON KEY LABEL CTRL+SPACEBAR DO rmouse *** *** pass: nothing *** Return: nothing **************************************************************** PRIVATE lcVarName ON KEY LABEL RIGHTMOUSE ON KEY LABEL CTRL+SPACEBAR lcVarName=upper(objvar()) && Current Active variable DO CASE *** Client Lookup options CASE lcVarName = "M.CLIENT" DO qk_menu with ; "\ to * Parameters: tdFrom: from date, defaults to date() * tdTo : to date, defaults to tdFrom Parameters tdFrom, tdTo Private lcSetReso, lnSelect * Validate parameters, supply default values tdFrom = iif(type('tdFrom') <> "D", date(), tdFrom) tdTo = iif(type('tdTo') <> "D", tdFrom, tdTo) lcSetReso = set('resource') && Save resource setting lnSelect = select() && Save original workarea set resource off && assumes it was 'ON' use (set('reso',1)) alias Diary in 0 noupdate select name as dDate, ; data as mContents, ; updated as dUpdated ; from Diary ; where ID = "DIARYDATA" and ; BETWEEN(name, DTOS(tdFrom),DTOS(tdTo)) ; into cursor DiaryData ; order by dDate report form ViewDiry preview use && Closes DiaryData use in Diary && closes opened resource file set resource &lcSetReso && Restore setting select (lnSelect) && restore workarea return 10. Been Thinking 'bout The Pretender(s)... ----------------------------------------------------------------------- ReplyTo: Whil Hentzen [70651,2270] (with apologies to Jackson Browne) I've been interviewing of potential employees for a high level programming position lately. A lot of folk still have a dBASE III mindset, but label themselves as "experienced FoxPro 2.5 programmers" simply because they're writing dBASE III code inside FoxPro/Win. It's easy to spend a lot of time just separating the "pretenders" from the "contenders" - and time is not one of those abundant resources.... As a result, I put together a set of questions that were designed to determine where an individual stood on the ladder of FoxPro 2.x programming proficiency. Note that I chose those words very carefully. I'm not looking for a developer - but a programmer. And I'm not just looking for the highest score possible, but rather, I want to understand the breadth of that individual's skill set so I can better evaluate the likelihood of their fitting in as a cog in my development environment's machine. These questions came about as a result of an evening's discussion at FoxTeach with, among others, Steven Black and Pat Adams, and then generated a lengthy thread on FoxGang afterward. The rationale behind the questions is in brackets behind each question.] 1. Versions of FoxPro. Name three major differences between 2.0 and 2.5. [How long have they been around?] 2. Language - Code a simple Foundation Read. Alternate: How do you get rid of the Command Window after you've put up a menu? [Are they still using DO WHILE? Have they even _heard_ of a FR?] 3. What is the difference between WHERE and HAVING? Describe how to implement an outer join in FoxPro. [A blank look here generates the prompt - "you know, in SQL" - which often leads to a "Oh, I only use FoxPro commands." And that's what I wanted to know .] 4. Name five FoxPro third party products and what they do. [Do they insist on inventing everything themselves?] 5. What periodicals do you regularly read? What books have you read? What else do you do to learn? [Do they understand the availability of resources available, or do they try to learn it all themselves?] 6. Screen Builder. Describe the tradeoffs of desnippetizing. Where do you keep your code? Name two valid reasons for modifying the SPR [Ha! Trick question!] 7. What's the difference between a function and a procedure? [Let's not forget the basics...] 8. What does this line of code do: private all like j* [Are they familiar with safe programming techniques?] 9. Describe your naming conventions for tables, fields, variables, arrays, windows. [A true mark of differentiation between hacks and professionals.] 10. What technique do you use to integrate browses with READs. [The best answer here is "Ugh, I can hardly wait until they make it a true READ object..."] 11. Name each band in the RW and describe when it is executed. [Are they still hand coding reports? Don't laugh - it happens....] 12. Describe the Transport process. [We do a lot of xplat work - have they?] 13. Write out a command to find a record that is Rushmore optimizable. Write one that is not. [Critical to find out if they understand how to take advantage of FoxPro's speed.] 14. Describe an alternative to PACK, when you'd use the alternative, and why. [Tells me if they've done any serious multi-user work.] 15. Describe what Set Default and Set Path do. [These are fundamental concepts in our environment. A lot of programmers still just throw all the programs and tables in a single directory.] 16. What's the difference between Build Project and Rebuild All. [Have they ever used the PM? And for what?'] 17. Tell me all you know about using Debug. [I _assume_ they've had bugs in their programs. Do they understand how to go about tracking them down efficiently?] Again, this isn't intended to be the ultimate FoxPro certification test. The purpose is to weed out, over the phone, the amateurs from those who've a solid foundation of knowledge. I can deal with a few weak spots; gaping holes, on the other hand, well, I'll let them work for my competition. I could go on, but the editor is making those slashing motions across his neck... 11. BCNN Statement of Ownership, Copyright, and Responsibility. ----------------------------------------------------------------------- The BCNN Newsletter is sponsored by the Foxpro User Group of the Boston Computer Society. BCNN is dedicated to keeping professional database developers (both consultants and corporate employees) informed about educational events, meetings, job openings, world events, notable articles, technical tips, new and 'must have' products, etc. As an electronic network BCNN is also a hub where developers can address world class issues with fellow developers around the world. Recipients agree to respond via Email to periodic polls of their directions, opinions, and needs. For those who do not have User Groups in their areas, BCNN is a vehicle for individuals to volunteer and contribute to something larger than themselves. The newsletter is distributed monthly by electronic mail via CompuServe, Internet, FidoNet, and other electronic gateways. It is free of charge to individual developers. Modest fees are charged to corporations for job placement and third-party announcements. Opinions expressed are solely expressed by the Foxpro User Group or the author found in the ReplyTo of the article. No warranties are made by the authors, editors, the Foxpro User Group or BCNN regarding the accuracy or applicability of the information provided in this newsletter, nor are the above named parties responsible for direct or incidental damages due to your use of this information. All materials are copyrighted by the BCS, unless otherwise indicated, and free for any user group to redistribute on their own BBS on the condition that a by-line referencing the BCS is included. Associate Editors: ---------------------------------------------------------------------- David Rose, Days (508)538-8064, Eves (617)935-6843. CIS:73164,2263 Internet:d_rose@necx.com Arnold Bilansky Days (617)522-3700 x374 CIS:71533,1031 Internet:71533.1031@CompuServe.Com Ted Roche, CIS 76400,2503 Computer Resource, Contoocook, NH (603) 746-5670 Submissions. ---------------------------------------------------------------------- Send submissions to 73164,2263 with the subject 'BCNN Foxpro Submission'. Format your submissions similar to this letter. Distribution and Subscription Services. ---------------------------------------------------------------------- Les Squires, Director, Xbase Language Group. LSquires@WJI.Com or 73020,3435 Add Subscribers: FOX-YES to LSquires@WJI.Com. Delete Subscribers: FOX-NO to LSquires@WJI.Com. Back Issues: Library 2 of FOXUSER forum (CompuServe) Search on "BCNN". FTP WJI.Com, Login as FTP, use your ID as the password, cd fox, copy all back issues. Boston Computer Society, Inc. 101 First Avenue Suite 2 Waltham, MA 02154 617-290-5700 General Number 617-290-5700 Ext. 432 for up-to-date meeting information. WWDN(tm) World Wide Developer Network Email Services donated by Word Jenny, Inc., Boston, Massachusetts USA LSquires@WJI.Com (c) 1994 Boston Computer Society, Inc.