Tame Your ActiveX Controls Doug Hennig - dFPUG-Portal
Tame Your ActiveX Controls Doug Hennig - dFPUG-Portal
Tame Your ActiveX Controls Doug Hennig - dFPUG-Portal
You also want an ePaper? Increase the reach of your titles
YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.
FoxTalk<br />
Solutions for Microsoft® FoxPro® and Visual FoxPro® Developers<br />
<strong>Tame</strong> <strong>Your</strong><br />
<strong>ActiveX</strong> <strong>Controls</strong><br />
<strong>Doug</strong> <strong>Hennig</strong><br />
6.0<br />
<strong>ActiveX</strong> controls are forward, but not backward, compatible. This means that if<br />
you install a newer version of an OCX on your system, any applications you ship<br />
to people who have an older version installed will break. This article presents a<br />
reusable tool that solves this problem in a simple, easy-to-use manner.<br />
HOW was the period between Christmas and New Year’s for you? I hope<br />
it was relaxing. For me, it was awful. The reason: I posted an update<br />
to Stonefield Database Toolkit (SDT) on our Web site just before<br />
Christmas. Anyone who downloaded and installed it immediately got an<br />
“OLE class not registered” error when trying to use SDT. I fielded a ton of<br />
support calls and e-mails the week after Christmas and had to come up with<br />
a fast solution to solve the problem for those folks and prevent even more<br />
calls later.<br />
What caused this problem? The main SDT form uses a TreeView control,<br />
and I had installed some software on my system that installed a newer version<br />
of COMCTL32.OCX, the OCX file containing the TreeView control. Even<br />
though I didn’t change the TreeView on the form, simply opening the form<br />
and saving it caused information about the newer control to be written to the<br />
form. Of course, very few others had this newer control on their machines, so<br />
even though the form wanted a TreeView control and they had a TreeView<br />
control, it wasn’t the exact control the form was looking for. Hence, the “OLE<br />
class not registered” error.<br />
Is it just me, or do you think this is incredibly lame too? I don’t care about<br />
different versions of the TreeView. I’d understand if I used new properties or<br />
methods of this control that didn’t exist in older versions, but as I mentioned, I<br />
didn’t even touch it in my copy of the form.<br />
I guess I only have myself to blame for being in crisis mode after<br />
Christmas. After all, I’d encountered this behavior before when I upgraded<br />
from VFP 5.0 to 5.0a; it came with an updated COMCTL32.OCX, and everyone<br />
Continues on page 3<br />
June 1998<br />
Volume 10, Number 6<br />
1 <strong>Tame</strong> <strong>Your</strong> <strong>ActiveX</strong> <strong>Controls</strong><br />
<strong>Doug</strong> <strong>Hennig</strong><br />
2 Editorial: Enter Sandman<br />
Whil Hentzen<br />
6 Best Practices:<br />
Development Checklist:<br />
Requirements Analysis<br />
Jefferey A. Donnici<br />
11 What’s Really Inside:<br />
Traversing Transactions<br />
Jim Booth<br />
12 Transactions: Tahoe and<br />
Microsoft Transaction Server<br />
Gary DeWitt<br />
15 Use the Microsoft Scripting<br />
Control to Script with VFP<br />
Rod Paddock<br />
18 The Kit Box:<br />
Synchronized Swimming<br />
Paul Maskens and Andy Kramek<br />
24 June Subscriber Downloads<br />
EA Cool Tool: The Best Tool<br />
is Knowledge<br />
Whil Hentzen<br />
Extended Article available at<br />
www.pinpub.com/foxtalk<br />
EA <strong>ActiveX</strong> and Automation<br />
Review: Where Should <strong>Your</strong><br />
Automation Code Reside?<br />
John V. Petersen<br />
Extended Article available at<br />
www.pinpub.com/foxtalk
Enter Sandman<br />
Whil Hentzen<br />
WANT to get depressed really quickly? The next<br />
time you’re around a group of your peers, ask<br />
everyone to name the supergroups of the ’90s.<br />
By “supergroup” I mean the true superstar bands—the<br />
Beatles, the Stones, Zeppelin, The Who, Cream, CSN&Y—<br />
you get my drift.<br />
Yeah, I know. It’s currently chic to deride Mick, Keith,<br />
et al. It was also chic to rip on Cream. But you can still<br />
hear Badge being played at 110 decibels at the McKinley<br />
Marina on Milwaukee’s Lakefront any summer<br />
weekend—until the police show up. I don’t hear Boy<br />
George or Vanilla Ice (“I’m going to be the next Elvis<br />
Presley”) much, and Green Day and Jane’s Addiction<br />
aren’t going to warrant a footnote in Rolling Stone’s<br />
Seventy-Five Years of Rock ’n Roll retrospective.<br />
But my point is that there have been no new<br />
supergroups since about the mid-’80s. U2 is about the last<br />
one I can think of. Yeah, Hanson and the Spice Girls can<br />
fill a stadium in three hours now. But so could The Babies<br />
and Bananarama. These days, it’s crank out a couple of<br />
albums and disband. Make your $10 million and bail.<br />
“These youngsters—they don’t know good music<br />
anymore.” I yearn for the days of good old-fashioned rock<br />
and roll, when singers danced with live pythons on stage<br />
or bit the heads off of doves.<br />
Bands today lack staying power.<br />
Editor Whil Hentzen, Editorial Advisory Board Scott Malinowski, Walter Loughney, Les Pinter, Ken Levy, Publisher Robert Williford,<br />
Vice President/General Manager Connie Austin, Managing Editor Heidi Frost, Copy Editor Farion Grove<br />
FoxTalk (ISSN 1042-6302) is published monthly (12 times per year)<br />
by Pinnacle Publishing, Inc., 1503 Johnson Ferry Road, Suite 100,<br />
Marietta, GA 30062. The subscription price of domestic<br />
subscriptions is: 12 issues, $179; 24 issues, $259. Periodical postage<br />
paid at Marietta, GA and at additional mailing offices. USPS#005373.<br />
POSTMASTER: Send address changes to FoxTalk, PO Box 72255,<br />
Marietta, GA 30007-2255.<br />
Copyright © 1998 by Pinnacle Publishing, Inc. All rights reserved. No<br />
part of this periodical may be used or reproduced in any fashion<br />
whatsoever (except in the case of brief quotations embodied in<br />
critical articles and reviews) without the prior written consent of<br />
Pinnacle Publishing, Inc. Printed in the United States of America.<br />
Brand and product names are trademarks or registered trademarks<br />
of their respective holders. Microsoft is a registered trademark of<br />
Microsoft Corporation. The Fox Head logo, FoxBASE+, FoxPro, and<br />
Visual FoxPro are registered trademarks of Microsoft Corporation.<br />
FoxTalk is an independent publication not affiliated with Microsoft<br />
Corporation. Microsoft Corporation is not responsible in any way for<br />
the editorial policy or other contents of the publication.<br />
This publication is intended as a general guide. It covers a highly<br />
technical and complex subject and should not be used for making<br />
decisions concerning specific products or applications. This<br />
publication is sold as is, without warranty of any kind, either express<br />
or implied, respecting the contents of this publication, including<br />
but not limited to implied warranties for the publication,<br />
performance, quality, merchantability, or fitness for any particular<br />
purpose. Pinnacle Publishing, Inc., shall not be liable to the<br />
purchaser or any other person or entity with respect to any liability,<br />
loss, or damage caused or alleged to be caused directly or indirectly<br />
by this publication. Articles published in FoxTalk reflect the views of<br />
their authors; they may or may not reflect the view of Pinnacle<br />
Publishing, Inc. Inclusion of advertising inserts does not constitute<br />
an endorsement by Pinnacle Publishing, Inc. or FoxTalk.<br />
Subscription information: To order, call Pinnacle Customer Service<br />
at 800-788-1900, or 770-565-1763. Cost of domestic subscriptions:<br />
12 issues, $179; 24 issues, $259. Canada: 12 issues, $194; 24 issues,<br />
$289. Outside North America: 12 issues, $199; 24 issues, $299.<br />
Individual issues cost $17.50 ($20 in Canada, $22.50 outside North<br />
America). All funds must be in U.S. currency.<br />
For European newsletter orders, contact: Tomalin Associates,<br />
Unit 22, The Bardfield Centre, Braintree Road, Great Bardfield, Essex<br />
CM7 4SL, United Kingdom. E-mail: 100126.1003@compuserve.com.<br />
Tel: +44 (0)1371 811299. Fax: +44 (0)1371 811283. 12 issues: £179<br />
For Australian newsletter orders, contact: Ashpoint Pty., Ltd.,<br />
9 Arthur Street, Dover Heights, N.S.W. 2030, Australia.<br />
Phone: +61 2-9371-7399. Fax: +61 2-9371-0180.<br />
E-mail: sales@ashpoint.com.au. Web: http://www.ashpoint.com.au.<br />
FoxPro technical support: Call Microsoft at 206-635-7191<br />
(Windows) or 206-635-7192 (Macintosh).<br />
Send all other questions or requests via the options at right.<br />
From the Editor FoxTalk<br />
And so does today’s software.<br />
I have a friend who runs the MIS department of a<br />
Fortune 200 company, and he’s getting ready to pull the<br />
plug on a bunch of boxes that have been collecting shop<br />
data (time clock type of info) since 1984. Same boxes,<br />
same software. None of this “upgrade of the month”<br />
business. None of this “make the chassis easily opened so<br />
you can replace broken parts in 30 seconds” rot. He’s<br />
replaced one fixed disk in 14 years. He’s not had to<br />
upgrade the boxes or change the software even once. And<br />
the best part? At the time he got these boxes, they’d been<br />
around for nearly a decade. (The equipment in question is<br />
the IBM Series 1, in case you were curious. And he’s going<br />
to replace them with Windows 95, er, Windows NT, uh,<br />
Windows 98, no, wait, make that NT 5, or . . .)<br />
I’ll bet you’re still doing a lot of the same stuff now<br />
that you were 10 years ago. Writing memos and technical<br />
documents. Calculating budgets and making cost<br />
estimates. Writing quote tracking, health care claim<br />
management, and accounting database applications.<br />
How much of the same software from 10 years ago are<br />
you still using? On the same machines?<br />
Boy, I wish it would stop. Or at least slow down a<br />
bit. I bet you do, too. But despite our most fervent wishes,<br />
it’s not going to. Why? It’s all these young kids in the<br />
industry who don’t have any perspective. “It can be done,<br />
Continues on page 22<br />
Editorial and<br />
Subscription<br />
Information<br />
770-565-1763<br />
800-788-1900<br />
E-mail<br />
foxtalk@pinpub.com<br />
Pinnacle Web Site<br />
http://www.pinpub.com<br />
Applies to VFP v6.0 (Tahoe)<br />
Applies to VFP v3.0<br />
Fax<br />
770-565-8232<br />
Mail<br />
PO Box 72255<br />
Marietta, GA<br />
30007-2255<br />
Applies to VFP v5.0<br />
Applies to FoxPro v2.x<br />
Accompanying files available online<br />
at http://www.pinpub.com/foxtalk<br />
Applies specifically to one of these platforms.<br />
2 http://www.pinpub.com<br />
6.0
<strong>Tame</strong> <strong>Your</strong> <strong>ActiveX</strong> <strong>Controls</strong> . . .<br />
Continued from page 1<br />
who hadn’t upgraded to 5.0a got “OLE class not<br />
registered” errors when they tried to use SDT. My<br />
workaround at the time was to keep one machine in our<br />
office with VFP 5.0 (not 5.0a) installed and do any SDT<br />
development involving that one form with the TreeView<br />
on it on that one machine. If I ever forgot and modified<br />
the form on my machine . . .<br />
However, knowing I’d experienced the problem<br />
before didn’t make me feel any better when my Inbox had<br />
dozens of e-mails reporting the problem. That was the last<br />
straw; I had to come up with a permanent solution to this<br />
problem. The tool described in this article is the result.<br />
Background<br />
First, let’s look at the cause of the problem. If you open a<br />
form containing an <strong>ActiveX</strong> control as a table (in other<br />
words, USE MYFORM.SCX) and browse it, you’ll find<br />
binary information stored in the OLE field of the record<br />
for the <strong>ActiveX</strong> control. This binary information appears<br />
to contain some version-specific information about the<br />
control. As a result—at least in the case of the <strong>ActiveX</strong><br />
controls shipped by Microsoft—<strong>ActiveX</strong> controls are<br />
forward but not backward compatible. This means that if<br />
I drop version 2.01 of an <strong>ActiveX</strong> control on a form,<br />
people with a newer version of the <strong>ActiveX</strong> control (such<br />
as version 2.02) can open the form, but those with an<br />
older version (like version 2.00) can’t.<br />
Let’s dig a little deeper and see how <strong>ActiveX</strong> controls<br />
are handled on a system. As you’re probably aware,<br />
simply copying an OCX file to someone’s hard drive<br />
doesn’t allow them to use the <strong>ActiveX</strong> controls contained<br />
in that file. The controls need to be registered on the<br />
system either using REGSVR32.EXE (a copy of this<br />
program comes with VFP) or letting VFP’s Setup wizard<br />
handle it for you. Using REGEDIT, we can see that<br />
<strong>ActiveX</strong> controls are registered by a class ID value in<br />
HKEY_CLASSES_ROOT\CLSID. For example, the<br />
TreeView control is registered in HKEY_CLASSES_<br />
ROOT\CLSID\0713E8A2-850A-101B-AFC0-<br />
4210102A8DA7. There are several subkeys under this<br />
key, the most important ones (as far as we’re concerned)<br />
being ProgID, VersionIndependentProgID, and<br />
InProcServer32. ProgID contains the program ID, which<br />
might change from version to version—for example, on<br />
my system, the value of this subkey for the TreeView<br />
is COMCTL.TreeCtrl.1. VersionIndependentProgID<br />
contains just what the name implies (on my system,<br />
the value is COMCTL.TreeCtrl for the TreeView), and<br />
InProcServer32 contains the name and path of the OCX<br />
containing the control. This immediately suggests several<br />
potential problems:<br />
• The <strong>ActiveX</strong> control was never installed on a<br />
machine—there’s no entry in the Registry for the<br />
control, and the OCX doesn’t exist on the drive. The<br />
solution is to properly install it.<br />
• The OCX file was copied to the system but not<br />
registered—the file exists, but there’s no entry in<br />
the Registry. The solution is to use REGSRV32.EXE<br />
to register the <strong>ActiveX</strong> controls in the OCX. Hint:<br />
You must include the extension for the file<br />
you’re registering:<br />
"regsvr32 myfile.ocx" instead of "regsvr32 myfile"<br />
• The <strong>ActiveX</strong> control is registered, but the OCX can’t<br />
be found—it might have been moved or renamed,<br />
or the user might have restored from a backed-up<br />
Registry but didn’t restore all files. The best solution<br />
is to reinstall and register.<br />
• The version installed on the system is different<br />
than the one required by an application. While an<br />
obvious solution to this problem is to install the<br />
version of the control required, this might not always<br />
be feasible. For example, if I shipped my copy of<br />
COMCTL32.OCX along with SDT, that would get rid<br />
of my problems but would probably cause a whole lot<br />
of problems for you and any clients you’d created<br />
forms with a TreeView for.<br />
Another unforeseen problem can also occur. Some<br />
<strong>ActiveX</strong> controls look for a license (a key stored in the<br />
Registry or a file on your drive) in order to use them in a<br />
development environment. This makes sense; it allows<br />
you to develop and distribute applications using a<br />
vendor’s controls and have those controls work in a<br />
runtime environment while protecting the vendor from<br />
someone else being able to develop using those controls<br />
without paying for them. The problem this causes,<br />
though, is that if the license key gets lost on your system,<br />
you’ll get a “License not found” error when you try to<br />
drop the control on a form. Reinstalling doesn’t always<br />
solve the problem. Based on messages I’ve seen on<br />
CompuServe and the Universal Thread, this problem has<br />
been a common one. For the controls shipped with VFP,<br />
the solution is simple: Download a file called FOX.REG<br />
from the Universal Thread (there’s a great reason to join<br />
right there) and run it; it will register the proper licenses<br />
on your system. For other third-party controls, reinstall or<br />
contact your vendor.<br />
As you can see, most of the problems regarding<br />
<strong>ActiveX</strong> controls require reinstalling them. However, as I<br />
mentioned, it might not be possible to solve the version<br />
problem this way. The rest of this article discusses an<br />
easy-to-implement solution to this annoying problem.<br />
SF<strong>ActiveX</strong><br />
The solution is to instantiate <strong>ActiveX</strong> controls at runtime<br />
rather than using them at development time. This way,<br />
http://www.pinpub.com FoxTalk June 1998<br />
3
the control is instantiated from the version installed on<br />
the user’s system rather than looking for the version that<br />
was installed on your system. To make this job easier, I<br />
created an <strong>ActiveX</strong> loader class called SF<strong>ActiveX</strong>.<br />
SF<strong>ActiveX</strong> is based on SFCustom, our Custom base<br />
class defined in our base class library SFCTRLS.VCX. It<br />
has the custom properties shown in Table 1.<br />
Table 1. Custom properties of SF<strong>ActiveX</strong>.<br />
Property Description<br />
cClass The name of the class defining the <strong>ActiveX</strong> control<br />
or a subclass of it<br />
cClassID The Class ID for the object<br />
cLibrary The name of the library containing a subclass of<br />
the <strong>ActiveX</strong> control<br />
cNewObjectName The name to give the <strong>ActiveX</strong> control this object<br />
will create<br />
cObjectName The name of the placeholder object to replace with<br />
the <strong>ActiveX</strong> control<br />
cOLEClass The OLE class for the object<br />
cClass defaults to “OLEControl” because that’s<br />
the VFP class that <strong>ActiveX</strong> controls are contained in.<br />
However, if you want to use a subclass of an <strong>ActiveX</strong><br />
control (I’ll show at least one reason why you’d want to<br />
do that later in the article), enter the name of the class in<br />
this property and the name of the library file containing<br />
the class definition in cLibrary (specify an extension to<br />
identify VCX from PRG libraries).<br />
You can specify the <strong>ActiveX</strong> control to use in one of<br />
two ways: by cClassID or cOLEClass. If you specify one,<br />
leave the other blank. cClassID is the class ID for the<br />
<strong>ActiveX</strong> control, and cOLEClass is its ProgID. How do<br />
you know what to enter for these properties for a given<br />
control? For the cOLEClass, the simplest way is to drop<br />
the desired control on a form, then look at its OLEClass<br />
property. For example, the OLEClass for the TreeView<br />
control is “COMCTL.TreeCtrl” (you might see<br />
“COMCTL.TreeCtrl.1” in your instance; however, don’t<br />
worry about the “.1” for cOLEClass). cClassID is a little<br />
harder to determine. You’ll need to fire up REGEDIT and<br />
search for the OLEClass for the control, then see what<br />
class ID it appears under. For example, the TreeView<br />
control appears under “{0713E8A2-850A-101B-AFC0-<br />
4210102A8DA7}”. You don’t need to enter the curly braces<br />
when you enter this value into cClassID. It’s better to<br />
specify cClassID than cOLEClass because the name of the<br />
control might change when different versions are installed<br />
(for example, the TreeView control that shipped with VFP<br />
5.0a has a ProgID of “COMCTL.TreeCtrl.1”, which could<br />
cause problems under certain circumstances if you<br />
specified “COMCTL.TreeCtrl”).<br />
Some <strong>ActiveX</strong> controls have a visual appearance, and<br />
others don’t. For those that do, you’ll need to specify the<br />
location and size of the control. You could do it in the Init<br />
of the form by setting the Top, Left, Width, and Height<br />
properties in code, but I find it more intuitive to put a<br />
Shape object on the form and size and position it where I<br />
want the <strong>ActiveX</strong> control to go. This object becomes a<br />
placeholder for the <strong>ActiveX</strong> control, making it easier to<br />
size, position, and place other objects in relation to it. To<br />
tell SF<strong>ActiveX</strong> that this object is the placeholder, enter its<br />
name in the cObjectName property. SF<strong>ActiveX</strong> will<br />
remove this object and put the <strong>ActiveX</strong> control in its place.<br />
SF<strong>ActiveX</strong> doesn’t have a lot of code. Its Init method<br />
calls the custom method Load<strong>ActiveX</strong> (which does all<br />
the work) unless told not to do so by passing .T. as a<br />
parameter (this is provided so you can instantiate<br />
SF<strong>ActiveX</strong> in code, set the properties as appropriate, and<br />
then call Load<strong>ActiveX</strong> manually to load the control). I<br />
won’t address all the code in the Load<strong>ActiveX</strong> method<br />
here (you can check it out yourself), just the more<br />
interesting stuff.<br />
SF<strong>ActiveX</strong> relies heavily on being able to look<br />
stuff up in the Windows Registry, because that’s<br />
where <strong>ActiveX</strong> controls installed on a user’s machine<br />
are registered. In order to work with the Registry,<br />
Load<strong>ActiveX</strong> uses the services of another class,<br />
SFRegistry, defined in SFREGISTRY.VCX. We won’t<br />
look at this class here; suffice it to say that I stole most<br />
of the code from REGISTRY.PRG, a Registry-handling<br />
class that comes with VFP, but made the programmatic<br />
interface a bit simpler.<br />
In order to use SFRegistry, you have to instantiate<br />
it; that work is done by a call to NEWOBJECT.<br />
NEWOBJECT.PRG is an updated version of<br />
NEWOBJ.PRG that I presented in this column in the<br />
September 1997 issue. It was updated to support the same<br />
syntax as the Tahoe NEWOBJECT function. This means<br />
that in Tahoe applications, you can omit this program,<br />
and any code calling it will work just fine.<br />
The first job for Load<strong>ActiveX</strong> is to figure out whether<br />
the desired <strong>ActiveX</strong> control is installed on the user’s<br />
system and, if the class ID was specified rather than the<br />
OLE class, which OLE class to use. The following code<br />
does this. Note that if you specify the OLE class and it<br />
can’t be found, Load<strong>ActiveX</strong> adds a “.1” suffix to the class<br />
and tries that before giving up. (The method starts with<br />
WITH THIS, so all unspecified object references are the<br />
class itself.)<br />
loRegistry = newobject('SFRegistry', ;<br />
'SFRegistry.vcx', cnHKEY_CLASSES_ROOT)<br />
if empty(.cOLEClass)<br />
lcCLSID = iif(left(.cClassID, 1) = '{', ;<br />
.cClassID, '{' + .cClassID + '}')<br />
lcProgID = loRegistry.GetKey('CLSID\' + lcCLSID + ;<br />
'\ProgID')<br />
llOK = not empty(lcProgID)<br />
else<br />
lcProgID = .cOLEClass<br />
lcCLSID = loRegistry.GetKey(lcProgID + '\CLSID')<br />
if empty(lcCLSID)<br />
lcProgID = .cOLEClass + '.1'<br />
lcCLSID = loRegistry.GetKey(lcProgID + '\CLSID')<br />
endif empty(lcCLSID)<br />
llOK = not empty(lcCLSID)<br />
4 FoxTalk June 1998<br />
http://www.pinpub.com
endif empty(.cOLEClass)<br />
if llOK<br />
lcOCX = loRegistry.GetKey('CLSID\' + lcCLSID + ;<br />
'\InProcServer32')<br />
llOK = not empty(lcOCX) and file(lcOCX)<br />
endif llOK<br />
At the end of this code, lcProgID contains the OLE<br />
class (ProgID), lcCLSID contains the class ID (which we<br />
don’t care about after this point), and lcOCX contains the<br />
name and path of the OCX file on the user’s system<br />
(found in the InProcServer32 key in the Registry). llOK<br />
is only .T. if we found what we were looking for in the<br />
Registry and the OCX file exists. If not, the user will get<br />
an error message, and this method will return .F.<br />
If everything is okay, Load<strong>ActiveX</strong> checks whether a<br />
placeholder was specified and, if so, saves its size and<br />
position and then removes it.<br />
llObject = not empty(.cObjectName)<br />
if llObject<br />
lcObject = .cObjectName<br />
with .Parent.&lcObject<br />
lnTop = .Top<br />
lnLeft = .Left<br />
lnWidth = .Width<br />
lnHeight = .Height<br />
endwith<br />
.Parent.RemoveObject(lcObject)<br />
endif llObject<br />
If a class and class library were specified,<br />
Load<strong>ActiveX</strong> opens the library (using either SET<br />
PROCEDURE or SET CLASSLIB, depending on the<br />
library’s extension). It then adds the <strong>ActiveX</strong> control or<br />
<strong>ActiveX</strong> subclass to the container, giving it the name<br />
specified in the cNewObjectName property. If a<br />
placeholder object was specified, the control is sized and<br />
positioned to the saved values. Notice the ProgID (stored<br />
in lcProgID) must be specified in the call to AddObject; if<br />
this was left out, the user would get a dialog box asking<br />
which control to insert. This is why we have to look in the<br />
Registry to determine the ProgID for the <strong>ActiveX</strong> control.<br />
lcObject = .cNewObjectName<br />
.Parent.AddObject(lcObject, .cClass, lcProgID)<br />
with .Parent.&lcObject<br />
if llObject<br />
.Top = lnTop<br />
.Left = lnLeft<br />
.Width = lnWidth<br />
.Height = lnHeight<br />
endif llObject<br />
.Visible = .T.<br />
endwith<br />
Using SF<strong>ActiveX</strong><br />
It’s easy to use SF<strong>ActiveX</strong>: Simply drop it on a form (or<br />
other container, such as a page in a PageFrame where the<br />
<strong>ActiveX</strong> control should go), fill in some properties, and<br />
put code in the Init method of the form to set any<br />
properties of the instantiated <strong>ActiveX</strong> control as desired.<br />
To make it even easier to use, I’ve created subclasses of<br />
SF<strong>ActiveX</strong> for the <strong>ActiveX</strong> controls I use most frequently:<br />
SFCommonDialog, SFImageList, and SFTreeView. These<br />
subclasses just have the appropriate class ID entered<br />
into the cClassID property so you don’t have to look it<br />
up. Drop one of these on a form to create the desired<br />
<strong>ActiveX</strong> control.<br />
Ah, but there’s one complication: What if you need to<br />
put some code in a method of the <strong>ActiveX</strong> control? This is<br />
frequently the case with visual controls—for example,<br />
with the TreeView and ListView controls, you need to take<br />
some action when the user clicks on a node or item in the<br />
list. In the case of the TreeView control, you’d put code<br />
into the NodeClick method. The problem, of course, is<br />
that you can’t add code to an object. The only way to<br />
accomplish this is to create a subclass of the <strong>ActiveX</strong><br />
control, put the desired code into the subclass, and tell<br />
SF<strong>ActiveX</strong> (via the cClass and cLibrary properties) to<br />
instantiate that class. One slight “gotcha”: You can’t create<br />
the subclass in a VCX because, like dropping an <strong>ActiveX</strong><br />
control directly on a form, VFP puts binary information<br />
about the control into the VCX, and that’s the whole cause<br />
of the problem SF<strong>ActiveX</strong> was designed to solve. Instead,<br />
you need to create the subclass in a PRG. Here’s the code<br />
from the sample ACLASSES.PRG that defines a subclass<br />
of the TreeView control; it has code in the NodeClick<br />
method (albeit simple code) that fires when the user clicks<br />
on a node. Note that OLEClass must be specified, or the<br />
user will get a dialog box asking which control to insert.<br />
define class MyTreeView as OLEControl<br />
OLEClass = 'COMCTL.TreeCtrl'<br />
Name = 'MyTreeView'<br />
procedure NodeClick<br />
lparameters toNode<br />
wait window 'You clicked on node ' + toNode.Text<br />
endproc<br />
enddefine<br />
Believe it or not, with all the problems we’ve solved<br />
using this scheme, we’re not quite out of the woods yet!<br />
There’s one last “gotcha”: Although you might think you<br />
could set some properties of the control in the subclass<br />
definition (such as Style and LineStyle in the case of the<br />
TreeView control), don’t do it. For a reason that escapes<br />
me, setting properties in the subclass seems to cause those<br />
properties to be carved in stone. Trying to change them<br />
later in a form the control is on fails completely. This<br />
means you have to set the properties after the control has<br />
been instantiated, usually in the Init of the form (which<br />
you can do, since by the time the form’s Init fires, the<br />
<strong>ActiveX</strong> controls have been instantiated). I’ll show an<br />
example of this in a moment.<br />
One final heads-up about the subclass: If you create<br />
an Init method in the subclass, it’ll need to accept a<br />
parameter. The OLE class is passed to the Init method<br />
(notice the third parameter in the AddObject call in<br />
Load<strong>ActiveX</strong>); although you don’t need it, you’ll get an<br />
error if you don’t have an LPARAMETERS statement.<br />
The sample TREEVIEW.SCX form shows how<br />
SF<strong>ActiveX</strong> is used. This form contains three objects: an<br />
SFTreeView object (SF<strong>ActiveX</strong> subclass specific for<br />
TreeViews) called oTreeViewLoader, an SFImageList<br />
object (SF<strong>ActiveX</strong> subclass specific for ImageLists) called<br />
oImageListLoader, and a Shape called shpTreeView that<br />
Continues on page 23<br />
http://www.pinpub.com FoxTalk June 1998<br />
5
Development Checklist:<br />
Requirements Analysis<br />
Jefferey A. Donnici<br />
With this month’s checklist column, Jeff takes a look at the<br />
process of gathering requirements for a development project.<br />
This column is the latest in a series of checklists designed to<br />
provide a starting point for your own development checklists.<br />
Building a set of system requirements is typically one of the<br />
first things to happen in a project and, as such, is an excellent<br />
opportunity to make sure that the project gets off on the<br />
right foot.<br />
AFTER covering coding, peer reviews, and a multipart<br />
segment on object-oriented design, I’m now<br />
going to take a look at the task of “gathering<br />
requirements.” This process in a development project can<br />
be labeled many things, and you might typically hear it<br />
called “requirements analysis,” “the analysis phase,”<br />
“specification construction,” or any number of variants.<br />
For the remainder of this column, I’ll simply use the term<br />
“analysis” to refer to the process of determining what the<br />
problem at hand is and what needs to be developed to<br />
solve that problem. In the world of object-orientation,<br />
this is a sub-field all its own, typically referred to as<br />
“object-oriented analysis.” Covering all the different<br />
concepts and ideas associated with object-oriented<br />
analysis is well beyond what can be covered in a single<br />
article, but I’ll list a couple of good references for those<br />
who are interested in learning more. While much of my<br />
own experience and focus these days is on object-oriented<br />
development, you might notice that not every item on<br />
this particular checklist is specific only to object-oriented<br />
projects. In fact, a number of the checklist items are<br />
applicable to nearly every software project, regardless<br />
of its development language, size, or cost.<br />
So, what exactly is “analysis”? Working backwards<br />
in the lifecycle of a software project, the “coding” is<br />
the process of implementing a software solution. The<br />
“design” phase of a project is used to determine how the<br />
system should be built (which parts it will contain and<br />
how they work together to form the system). The<br />
“analysis” phase, however, is the process of determining<br />
what needs to be built. In other words, an analysis process<br />
is meant to figure out the scope of the problem that the<br />
software must solve. I like to think of it as a process of<br />
“putting limits on the project” because the analysis phase,<br />
more than any other, will determine the scope of what the<br />
Best Practices FoxTalk<br />
system will (and won’t) do.<br />
Like any other process, the analysis phase of a project<br />
needs to have a result or end product. Typically, this will<br />
come in the form of a “requirements specification” that<br />
details what the system is expected to do. The form of<br />
these specifications can vary from project to project,<br />
depending on project size and a variety of other factors,<br />
but it might consist of a three-page narrative describing<br />
the system or a multi-binder set of diagrams, tables, and<br />
plain-English text. It’s difficult to know in advance when<br />
enough requirements information has been gathered, but<br />
you’ll definitely know it after the fact. If the developers<br />
working on the design phase of a project keep going back<br />
to the clients (or others involved in the analysis) for more<br />
information or clarification, then you know that the<br />
specification could have been more complete.<br />
With these ideas and definitions out of the way,<br />
let’s get started on the checklist of issues to watch out<br />
for during this early phase of a project.<br />
Is a true “analysis effort” being made<br />
at the start of the project?<br />
Yes, I know it sounds incredibly obvious to bring this up,<br />
but many projects get started with someone sitting down<br />
to write code and “prototype” the application in advance.<br />
It’s important that everyone involved with the project be<br />
aware of the importance of doing the analysis and<br />
gathering requirements. Even the smallest of software<br />
applications is intended to solve some sort of problem, so<br />
make sure there’s a clear understanding of what that<br />
problem is before beginning any design or coding effort.<br />
This can save a lot of embarrassment, headache, and,<br />
most importantly, money by avoiding situations wherein<br />
a certain “something” gets developed that isn’t at all the<br />
“something” that the end-users truly need.<br />
Unfortunately, some management types have the<br />
impression that a software developer can just sit down,<br />
close the door, push a few buttons, and magically deliver<br />
the software that management sees in their heads. There<br />
isn’t always a lot you can do to convince these people<br />
otherwise, but you can try to get the eventual users of the<br />
software involved early to make it clear that some form of<br />
requirements gathering needs to be performed. These are<br />
the people who will likely be “living with” your software<br />
6 FoxTalk June 1998<br />
http://www.pinpub.com<br />
6.0
on a daily basis and, presumably, will have their jobs<br />
made easier through your efforts. They naturally have<br />
a vested interest in making sure the developer(s)<br />
completely understands the problem domain that the<br />
software will address. Their involvement will make it<br />
clear to management that it’s important for you to have<br />
as much clarity as possible when it comes to the true<br />
goals of the software.<br />
Are all of the necessary people being involved to<br />
define the system’s intent and requirements?<br />
In light of the preceding section, it’s important to realize<br />
that it really takes a variety of people, at a variety of<br />
organizational levels, to make a software project<br />
successfully meet the needs for which it’s intended.<br />
Typically, you’ll have the management types who approve<br />
the project and ultimately compensate you for your work.<br />
Whether you’re a corporate developer or an independent<br />
consultant makes no difference other than the name that<br />
appears at the top of everyone’s paychecks. Since these<br />
are the people paying you, you want to make sure they’re<br />
involved, happy, and see progress happening. As I’ve<br />
already mentioned, you also need the end-users to be<br />
represented in the analysis phase because they’re going to<br />
be putting the fruits of your labor to work. While these<br />
people probably don’t approve your compensation, I tend<br />
to think they’re more important to the developer than the<br />
management type. That’s because they’re likely to be<br />
vocal when it comes to judging the results of your work.<br />
A manager who’s happy with your efforts isn’t likely to<br />
stay that way if he’s got an end-user mutiny on his hands.<br />
The point here is that it’s important for both types of<br />
people to be involved. While the end-users can ultimately<br />
tell you what needs to be done, the managers must be<br />
involved to tell you the constraints within which it must<br />
be done (budgets, schedules, and so forth).<br />
If possible, try to get feedback from more than just<br />
one “future end-user” for your system. If you’re getting<br />
all of your requirements and input from one person, you<br />
run the risk of developing software that meets the needs<br />
of the way that one person works. This can come at the<br />
expense of everyone else who must use the system,<br />
many of whom may have different needs or methods for<br />
approaching their job. Where I work, we write software<br />
applications for a wide variety of clients in the energy<br />
industry and ship that software with large volumes of<br />
data. The purpose of our software is to give the users a<br />
variety of tools and mechanisms for interpreting the<br />
data that they’ve licensed. One of our internal product<br />
managers will often come up with a variety of new ideas<br />
and functionality for his product and then explain to me<br />
that he’s “the typical end-user” for that product. Much as<br />
I wish he were “typical,” he’s significantly more familiar<br />
with the energy industry and our data than any “typical<br />
end-user” is, and that has to be factored into any requests<br />
or suggestions he makes. The point I’m hoping to make<br />
here is that it’s important to “know the audience” for<br />
your software application. With the input of only a small<br />
cross-section of the potential user base, you run the risk of<br />
developing software that meets the needs and desires of<br />
the few—at the expense of the many.<br />
Another point that’s worth making concerns other<br />
developers. If possible, get the people who will be doing<br />
the system design and implementation involved with the<br />
analysis effort. While it’s nice to have so complete a<br />
specification that it can be handed with confidence to a<br />
designer who wasn’t involved with that process, having<br />
other developers there can add clarity in the later phases<br />
of the project. They might ask different questions than the<br />
person assigned the requirements responsibility, and<br />
asking questions is the analysis phase.<br />
Has a common vocabulary been defined<br />
for developers and users so they can<br />
clearly communicate?<br />
Every type of business and job function has its own<br />
language. As a software developer, it’s likely that you’ll be<br />
exposed to a wide variety of businesses and industries.<br />
Personally, I know that’s one of the most exciting aspects<br />
of software development for me. As you get into each new<br />
project, however, you’re probably going to hear a variety<br />
of new terms and even some old terms that have<br />
meanings other than the definition you’re familiar with.<br />
As you’re meeting with the other parties involved with<br />
the project, make sure to stop and get definitions and<br />
explanations whenever they refer to something that<br />
you’re not clear on. It’s pretty common for this process to<br />
begin with someone saying, “Let’s start with the basics,”<br />
as they proceed to give you the background information<br />
they think you need. If you’re not completely clear on that<br />
background information, you’re really going to be lost<br />
when they get to the explanation of their problem. Once<br />
again, the point of an analysis effort is to get questions<br />
answered, so make sure to ask them early. Having a<br />
shared vocabulary established early in the process will<br />
make all communications much easier throughout the life<br />
of the project.<br />
This is also a good time to point out the importance<br />
of remembering that not everyone you’re working with<br />
in this phase of a project is a software developer. Terms<br />
like “inheritance,” “encapsulation,” and “referential<br />
integrity” might be commonplace in this newsletter and<br />
at programming conferences, but they don’t mean a<br />
thing to your average computer user or business<br />
manager. Because this phase of a project is primarily<br />
for the end-users, try to keep the development-related<br />
technical jargon to a minimum and speak to them in<br />
their language. You can translate their needs and desires<br />
into your programming terms in the design phase of<br />
the project.<br />
http://www.pinpub.com FoxTalk June 1998<br />
7
Is a completely different approach being forced<br />
on the eventual users of the system?<br />
The typical goal of a software project is to accurately<br />
model some process that the end-users would otherwise<br />
have to go through manually so that their job is ultimately<br />
made easier. The chances are pretty good that these endusers<br />
are already performing the task that the software<br />
will model, and they’re used to doing it in a certain way<br />
or using a specific methodology (perhaps even with an<br />
existing software application). As the analysis phase of a<br />
project begins, it’s not uncommon for users to come up<br />
with new ways of approaching their job, sometimes at the<br />
suggestion of the developer gathering the requirements<br />
(no hidden motives there, huh?). When the analysis of<br />
the new system begins to look like a completely new<br />
approach for the business process, proceed very carefully.<br />
As much as I hate to borrow an incredibly over-used<br />
term, what you’re watching out for here is a “paradigm<br />
shift” in the approach the software takes to solve the<br />
business problem.<br />
I should clarify that this caveat isn’t intended to stifle<br />
anyone’s innovation or creativity. I think that users are<br />
generally excited by being a part of the requirements<br />
gathering process, and it’s easy for them to lose sight of<br />
the fact that this software will likely become either a very<br />
good friend or another drudgery added to their day. If the<br />
approach the software uses to solve a problem isn’t a<br />
sound or “tried and true” approach that the users can<br />
work with, they’re not going to be happy once the system<br />
is deployed and the “honeymoon period” is over. The one<br />
area that I think is a slight exception to this rule is when a<br />
shift will mean a large increase in the user’s efficiency.<br />
Anyone can get used to something that makes their day<br />
easier or frees up more of their time, but make sure that<br />
changing the “business process” isn’t going to ultimately<br />
have the opposite effect. Innovate where it makes sense,<br />
but don’t change things on the user purely for the sake<br />
of change.<br />
Have all of the necessary—as well as potentially<br />
new—components of the system been discussed?<br />
With some projects, you’ll be writing a system that<br />
replaces an older, obsolete, or possibly cumbersome<br />
system that’s already in place. Other times, you’ll be<br />
creating an application from scratch to streamline an<br />
existing business process. In the latter case, the users and<br />
their managers might not have any idea what they need<br />
or could use until you come right out and suggest it. In<br />
the former case, they might be so used to dealing with the<br />
existing software that they don’t see past it for new ideas<br />
and functionality. For that reason, I’m using this checklist<br />
item as a catch-all to list the types of things you might<br />
suggest to them. These are components that appear in<br />
many different types of applications and might or might<br />
not be important for any given project.<br />
For starters, do they have any existing reports,<br />
spreadsheets, or documents that the system will need to<br />
be capable of generating or reading? If there’s already a<br />
system in place, should the existing data be converted to<br />
a new format, or should it simply be archived so they<br />
can start fresh? Should the application have any security<br />
mechanisms built into it and, if so, to what degree? Now<br />
that everyone and their pet has an e-mail address, should<br />
the application be capable of sending (or even receiving)<br />
e-mail messages? Is there a need for users to do offline<br />
work with their data, separated from the primary data<br />
sources? Could they use remote access capabilities in<br />
the application? Do they have a need for specific file<br />
structures or file types? Will the system need to have<br />
any export or import capabilities? Does the system need<br />
backup functionality built into it, or will they use some<br />
other backup solution? How much flexibility do they<br />
need for customizing the user interface or business<br />
logic within the system? Do they need access to file<br />
maintenance capabilities in the system, or should those<br />
be handled automatically? To what extent should the<br />
system provide online help or user assistance? Should<br />
the help system only aid them with the use of the system,<br />
or should it also provide assistance with the business<br />
process that the system is designed to model?<br />
Obviously, there are a lot of these types of questions<br />
to be asked when you sit down to work out the<br />
requirements for a new software application. Trying to<br />
come up with an exhaustive list would be impossible for<br />
me to do here, but the preceding suggestions should give<br />
you some ideas of the types of things that might not occur<br />
to users until they’re suggested. Going on the assumption<br />
that you want this application to be as useful as possible<br />
for your end-users, ask lots of questions and make sure<br />
you’re clear enough on the answers to meet their needs<br />
with a true solution.<br />
Has the specification been built with scalability<br />
and future expandability in mind?<br />
An application that can “scale up” as the needs placed on<br />
it grow must be intended that way from the onset. While<br />
it’s usually an interesting “employee interview” question,<br />
you might find it useful to find out where the users and<br />
buyers of this system expect themselves and the software to<br />
be in a couple of years. You might be writing a small,<br />
standalone sales system for a bookstore today, but what if<br />
they plan to grow into a new space next year and have<br />
three different sales computers? Most businesses have<br />
growth as a primary goal, so it’s important to find out<br />
how much expansion capability the users and managers<br />
would like to see. Chances are that your software will<br />
need to be replaced if they become the next “Barnes &<br />
Noble” or “Amazon.com,” but you don’t want it to be<br />
obsolete as soon as they need a little more storage<br />
capability either.<br />
I should note here that I’m not referring specifically to<br />
the software’s internal flexibility and expandability. That<br />
8 FoxTalk June 1998<br />
http://www.pinpub.com
comes as a function of the design phase in the project and<br />
should be built into every system you work on. Even if<br />
the business never grows, the needs of the software might<br />
simply change, and it’s important to have that capability<br />
inherent in the components of the application. Instead,<br />
this topic is intended to get you thinking about the actual<br />
growth that a system might undergo. For example, you<br />
might write a relatively simple application for a small<br />
business that doesn’t have any networking or multi-user<br />
needs at all. One day, the owner calls you and says he’d<br />
like to be able to access the software from home so that he<br />
doesn’t have to go into the office every day. That’s easy<br />
enough, but he’d like to be able to do that even if one of<br />
his employees is in the office and using the system.<br />
Suddenly that simple application you built needs remote<br />
access and multi-user capabilities. A year later, the owner<br />
calls you and tells you he needs the software to be<br />
running around the clock with zero downtime, but he also<br />
wants nightly backups made of all that data in the<br />
background. Now it makes more sense for the Visual<br />
FoxPro application you built to be using SQL Server as a<br />
back-end instead of native VFP tables. Suddenly all the<br />
data-related code you wrote won’t do, and you wish<br />
you’d been using view definitions from the beginning.<br />
Once again, these are issues that can be resolved with a<br />
solid application design effort, but it’s good to get them<br />
out in the open during the analysis phase, if only to get<br />
feedback and suggestions from the true “owners” of<br />
the system.<br />
Have all “use cases” and “actors” that<br />
might be required within the system<br />
been discussed and clarified?<br />
In his book Object-Oriented Software Engineering: A Use<br />
Case Driven Approach (Addison-Wesley, ISBN 0201544350),<br />
Ivar Jacobson discusses the idea of using “use cases” and<br />
“actors” to work out the requirements of an objectoriented<br />
application. A “use case” is roughly defined as a<br />
description of the tasks that must be completed as part of<br />
an overall process. For example, an automated teller<br />
machine might have a “withdraw cash” use case, a<br />
“deposit check” use case, and a “balance inquiry” use<br />
case. On its own, the description of a use case doesn’t<br />
have much value other than to provide a convenient way<br />
to refer to a larger process or series of steps. An “actor” is<br />
described as an “external agent” or “user” involved with<br />
the process that a use case describes. For example, a<br />
purchasing system for a company might have a “fulfill<br />
order” use case as one of its needs. The steps involved<br />
with this process might include an employee filling out a<br />
request form, a supervisor approving the request, a<br />
purchaser making the purchase, a supplier providing the<br />
requested materials, and a mailroom employee delivering<br />
it. Each of these persons is considered an “actor” within<br />
the “fulfill order” use case because the process isn’t<br />
complete until each of them has performed his or her role.<br />
My goal here isn’t to convince you that Jacobson’s<br />
approach is the best or only way to go for your own<br />
analysis, though I do highly recommend his book.<br />
Instead, it’s to provide you with an additional way to<br />
determine whether or not all the questions have been<br />
asked when analyzing the needs of a system. In truth, I<br />
have to agree with the author of another excellent book—<br />
Object-Oriented Software Construction, Second Edition by<br />
Bertrand Meyer (Prentice Hall, ISBN 0136291554)—in<br />
suggesting that unless a development team has plenty of<br />
experience developing object-oriented applications, the<br />
use case approach can be somewhat limiting during the<br />
analysis and design phases of a project. As Meyer points<br />
out, the use case approach requires that the process be<br />
described in a fairly rigid fashion with a specific sequence<br />
of events. This can be at odds with the abstract, stateless<br />
approach that an object-oriented application should<br />
take—an approach where various self-contained objects<br />
can receive a type of request and proceed to work in<br />
conjunction with other objects as necessary to meet that<br />
request. As such, I don’t believe that a use case approach<br />
should be the only method of determining the needs of an<br />
object-oriented application. I do, however, think that use<br />
cases can be a valuable tool for looking at a system’s<br />
requirements from a new and unique perspective. When<br />
taking the time to describe a system as a series of use<br />
cases, each of which requires a set of actors for<br />
completion, you might find that an important<br />
requirement or consideration has been overlooked.<br />
Are there clear limits to what the system<br />
will and won’t do when complete?<br />
Here’s another seemingly obvious item that I think is<br />
often overlooked in requirements gathering efforts. It’s<br />
common for the analysis to indicate everything that an<br />
application should do, but much less common is for some<br />
attention to be devoted to what it shouldn’t do when<br />
complete. For example, if your application must have an<br />
option to let the user export a table to another file format,<br />
should the user also have the ability to choose which<br />
fields in the table are exported? If the application has a<br />
charting or graphing component, will it also have the<br />
ability to export the chart as a graphic file?<br />
When the developers working on the design effort get<br />
the results of the analysis phase, they’re naturally going to<br />
have questions about the extent to which they should<br />
account for the unknown. When possible, indicating what<br />
the application doesn’t need to do can help them rule out<br />
some of those unknowns and focus more clearly on the<br />
things that the application does need.<br />
Is there an end result to all of the analysis<br />
and requirements work?<br />
A requirements effort is really only as good as the<br />
specifications and documentation it generates at the<br />
end of the process. Getting a variety of people involved,<br />
http://www.pinpub.com FoxTalk June 1998<br />
9
combining opinions for a consensus, and determining<br />
every last need for a software project is great, but it’s<br />
useless without some way of communicating those<br />
results to the people who will design and implement<br />
the solution.<br />
As I mentioned earlier in this column, the end result<br />
for an analysis effort could be anything from a free-form<br />
memo that’s a few pages in length to a truckload of<br />
documentation that details every last need in the finest<br />
level of detail. Where your needs fall within those<br />
extremes should be determined early in the analysis<br />
phase so that everyone knows going in what’s expected at<br />
the end of the process. Make sure that the specifications<br />
are provided in writing and don’t consist of a<br />
“specification meeting” with the people who will<br />
design and implement the system. Invariably, intents<br />
and desires will be left out of a meeting format, and the<br />
miscommunications that result can be the death of a<br />
software project.<br />
Have all the necessary people approved<br />
the final analysis results?<br />
Once you’ve got the requirements put together and put in<br />
writing, make sure that everyone who has an interest in<br />
the project has “signed off” on the results. This doesn’t<br />
need to be a formalized process with people signing their<br />
lives away, but they should have an opportunity to review<br />
and make comments on the results of the process they’re<br />
involved in.<br />
Getting the results of the process approved by<br />
everyone greatly increases the chance of success for a<br />
project because it avoids the “it does what I said, but not<br />
what I meant” problem that often pops up during a<br />
project. When someone reads through the description of<br />
their needs later, well after they initially expressed those<br />
desires, they can often clarify those thoughts so that what<br />
they’re agreeing to is truly what they want. Having<br />
management review the requirements will ensure that the<br />
system requirements are in line with the goals of the<br />
business, and having the end-users take part in the review<br />
will make sure that the application will truly be of use to<br />
them in their job functions. In short, it’s just a matter of<br />
making sure that everyone with a vested interest in the<br />
project is on the same page. <strong>Your</strong> development staff<br />
thanks you for this. Trust me.<br />
Wrap it up<br />
That brings to a close my list of items to watch out for<br />
during the analysis, or requirements gathering, phase of a<br />
software project. I know it’s by no means an exhaustive<br />
list, but I hope it triggers a variety of new ideas and<br />
checklist topics for you to use on your own. As always,<br />
feel free to send me your feedback and thoughts on this<br />
(or any other) topic, as well as your suggestions for future<br />
checklists or columns.<br />
If you’re interested in reading some more in-depth<br />
discussions of the analysis phase, I highly recommend<br />
each of the books I’ve mentioned in this column. And<br />
speaking of great books, I finally got around to finishing<br />
What Every Programmer Should Know About Object-Oriented<br />
Design by Meilir Page-Jones (Dorset House, ISBN<br />
0932633315). Right there at the end of the book, in<br />
Appendix A, was an exhaustive checklist of 51 things to<br />
watch for during an object-oriented design. If you were<br />
left hungry for more ideas or in-depth discussion after the<br />
object-oriented design checklists I presented over the past<br />
two months, I’d definitely recommend this book. It’s<br />
written in a very readable, light style and covers the<br />
design phase of a project in great detail. It just occurred<br />
to me that, while my last couple of columns might or<br />
might not have provided any additional food for thought,<br />
they could have inadvertently blown your budget for<br />
“development books.” I should probably stop here and<br />
give some thought to making next month’s column the<br />
kick-off for a new 12-step program for book addicts like<br />
myself. See you then! ▲<br />
Jefferey A. Donnici is an applications developer who specializes in<br />
reusability and design issues with Resource Data International, Inc. in<br />
Boulder, CO. Jeff is a Microsoft Certified Professional in Visual FoxPro<br />
and a three-time Microsoft Developer Most Valuable Professional.<br />
303-444-7788, fax 303-444-1286, jdonnici@compuserve.com,<br />
jdonnici@resdata.com.<br />
10 FoxTalk June 1998<br />
http://www.pinpub.com
Traversing Transactions<br />
Jim Booth<br />
Last month, Jim discussed TableUpdate() and TableRevert()<br />
and showed how they enable you to control updates while<br />
using buffering on your cursors and tables. These two<br />
functions are really very good for handling the updating or<br />
reverting of a single table or cursor. This begs the question,<br />
“How can I coordinate the updates or reverts to a number of<br />
related tables?” Gary DeWitt discussed transactions in the<br />
January and March 1998 issues of FoxTalk in preparation for<br />
MTS; this month, Jim reviews that and discusses what can fail<br />
during a transaction.<br />
THERE are situations where you need to update more<br />
than one table or cursor, and the combination of the<br />
updates represents one action on the database. This<br />
action has to be an “all or none” operation—that is, either<br />
all of it happens or none of it happens. For example, in<br />
writing the code to save a new invoice, you need to add<br />
a record to the invoice header table, add a group of<br />
records to the invoice line item table, update the balance<br />
in the customer record, and add a record to the accounts<br />
receivable table. If any of these operations fail, you’ll need<br />
to reverse the earlier ones that did succeed and make sure<br />
that none of these updates get into the data. Transactions<br />
are designed for this very purpose.<br />
You use the BEGIN TRANSACTION command to<br />
start the transaction tracking in Visual FoxPro, and the<br />
ENDTRANSACTION command to complete the<br />
transaction or the ROLLBACK command to reverse<br />
the transaction.<br />
How do transactions work?<br />
When VFP encounters a BEGIN TRANSACTION, it starts<br />
to track the file update operations that occur. For each<br />
update action, VFP secures the required locks, but it<br />
doesn’t do the update yet. When an ENDTRANSACTION<br />
is encountered, VFP then does all of the file writes<br />
involved in the transaction and releases the locks that<br />
were obtained. If a ROLLBACK command is encountered,<br />
VFP will release the locks without doing any of the file<br />
write operations.<br />
Through this process, VFP is able to assure that either<br />
all of the file updates are done or none of them are done.<br />
It’s a very good idea to keep the BEGIN TRANSACTION<br />
and ENDTRANSACTION/ROLLBACK commands as<br />
physically close to each other in the code as possible.<br />
Doing this reduces the period of time that the resources<br />
are locked.<br />
What’s Really Inside FoxTalk<br />
Which tables and cursors<br />
can participate in a transaction?<br />
Transactions in VFP are only available for tables that are<br />
in a database (DBC). Free tables can’t participate in a<br />
transaction, and a rollback or other abnormal end of the<br />
transaction doesn’t affect any updates made to free tables<br />
during a transaction.<br />
This can cause some interesting effects if a free table is<br />
the source of data for a local view that’s managed by a<br />
transaction. I’ll examine the steps in the process of a<br />
fictitious transaction—in this transaction, there’s one table<br />
in a database named Table1 and a view named View1 that<br />
gets its data from a free table named Table2. Here’s the<br />
code for the transaction:<br />
BEGIN TRANSACTION<br />
IF TableUpdate( 1, .F., "View1")<br />
IF TableUpdate ( 1, .F., "Table1")<br />
END TRANSACTION<br />
ELSE<br />
ROLLBACK<br />
ENDIF<br />
ELSE<br />
ROLLBACK<br />
ENDIF<br />
If the TableUpdate for Table1 fails, will the<br />
ROLLBACK undo the changes to View1 and set it back to<br />
its state prior to the transaction? Yes, it will roll back the<br />
changes to View1. Will the changes to Table2 that were a<br />
result of updating View1 be reversed by the ROLLBACK?<br />
No, because Table2 is a free table and therefore isn’t<br />
participating in the transaction. This can leave your data<br />
in an undefined state, so you have to manage the free<br />
table yourself. It’s best not to depend on transactions<br />
when free tables are involved in any way.<br />
Show us the code!<br />
How do you integrate transactions into your forms? It<br />
can be done on a form-by-form basis, or you can do it<br />
generically in a form class. The following is some<br />
pseudo-code that will give you the outline of the structure<br />
for incorporating transactions into your updates.<br />
* Set a variable for tracking a failure.<br />
LOCAL llRollBack<br />
llRollBack = .F.<br />
* Wait until all data processing is complete<br />
* before beginning the transaction.<br />
BEGIN TRANSACTION<br />
* Now do each update checking the result.<br />
IF NOT TableUpdate( … )<br />
llRollBack = .T.<br />
ELSE<br />
Continues on page 23<br />
http://www.pinpub.com FoxTalk June 1998<br />
11
Transactions: Tahoe and<br />
Microsoft Transaction Server<br />
Gary DeWitt<br />
Component reuse is one of the key goals of object-oriented<br />
programming and one of the difficulties with transaction<br />
processing—until now. Here’s how Tahoe and Microsoft<br />
Transaction Server can work together to create and deploy<br />
robust, reusable transactional components.<br />
AT the First National Bank of Dad, the CFO, Dad,<br />
has realized he can no longer give his account<br />
holders personalized service. Instead, he’ll install<br />
an ATM (automatic teller machine) to handle the everincreasing<br />
demand for the bank’s services. The ATM must<br />
allow a depositor to deposit his or her allowance into<br />
savings or checking accounts, withdraw allowance from<br />
either account, and transfer allowance from one account<br />
to the other.<br />
In the January 1998 issue of FoxTalk (see<br />
“Transactions: Take the ACID Test”), I introduced you to<br />
transaction processing at the First National Bank of Dad. I<br />
described an account object with two methods: Deposit()<br />
and Withdraw(). Each of these methods uses transactions<br />
to ensure that the database is updated correctly. The<br />
transaction is started and ended within the context of a<br />
deposit or a withdrawal.<br />
In the March 1998 issue of FoxTalk (see “Transactions:<br />
Keeping Them in Context”), I discussed ways that the<br />
account class can be written so that it can work in the<br />
context of a funds transfer, where both a deposit into one<br />
account and a simultaneous withdrawal from another<br />
account must both succeed or both be rolled back. You<br />
learned about nested transactions and the often<br />
complicated ways of writing components to work in the<br />
context of other transactions. Writing robust, reusable<br />
transactional components for client/server databases<br />
can be particularly difficult. I ended by wishing for a<br />
better way.<br />
A better way<br />
Warning: You need to know this stuff. The techniques I<br />
discuss in this article are applicable to client/server<br />
databases, but there are some key events on the horizon<br />
that are likely to make you want to do more client/server<br />
applications, even for desktop deployment.<br />
I recently visited Microsoft’s Web site (http://<br />
www.microsoft.com) and executed a search on the phrase<br />
FoxTalk<br />
“SQL Server 7.” I found numerous Web pages,<br />
presentations, and press releases about the next version<br />
of Microsoft SQL Server, code-named Sphinx. One of the<br />
key features I found for SQL Server 7 is scalability. Not<br />
only will it scale to very large databases, but to very small<br />
ones too. One PowerPoint presentation on the Microsoft<br />
site called this “SQL Desktop” and “Embeddable SQL.”<br />
According to Microsoft, SQL Server 7 will run under both<br />
Windows NT and Windows 9x. The embedded version<br />
will have a small footprint, and you’ll be able to distribute<br />
it with your applications to run on desktop systems and<br />
even laptops. It will finally be not only desirable, but<br />
also possible to use SQL Server for small database<br />
applications.<br />
The other key event is the release of Tahoe. Tahoe<br />
allows you to write components that take full advantage<br />
of MTS (Microsoft Transaction Server) and Microsoft<br />
SQL Server.<br />
What is MTS?<br />
MTS, currently in version 2.0, is a powerful environment<br />
that makes it easier to develop and deploy robust, highperformance<br />
transactional components. Last September at<br />
the Visual FoxPro DevCon, Microsoft demonstrated some<br />
of the new features of Tahoe, including its ability to work<br />
with MTS. I was so excited about the possibilities that I<br />
rushed home and immediately started experimenting<br />
with MTS 1.0, SQL Server 6.5, and Visual Basic 5.0. As I<br />
write this (in early April), I’ve been working with MTS<br />
2.0, SQL Server 6.5, and Tahoe nearly every day for<br />
about a month.<br />
Here are some of the advantages I’ve discovered<br />
from using this combination of tools: Well-written<br />
components can be reused in any transactional context<br />
with no modifications; valuable resources, such as ODBC<br />
connections, can be pooled with no modifications; granular,<br />
easy-to-develop components can be combined with no<br />
modifications; complicated distributed transactions can be<br />
implemented as easily as simple ones with no modifications;<br />
performance of frequently used components can be<br />
enhanced with no modifications.<br />
At the risk of being accused of belaboring my point,<br />
let me reiterate that all these advantages can be achieved<br />
in components written for MTS with no modifications.<br />
12 FoxTalk June 1998<br />
http://www.pinpub.com<br />
6.0
Basic MTS transactional components<br />
MTS components you create with Tahoe are in-process<br />
servers. Unlike other in-process servers, these run in a<br />
Transaction Server process, rather than your application’s<br />
process. MTS assigns transactional components a<br />
transaction context. <strong>Your</strong> components can get an object<br />
reference to the transaction context by using one of the<br />
MTS <strong>ActiveX</strong> servers.<br />
*-- Obtain a reference to transaction context.<br />
oServer = CREATEOBJECT("MTXaS.Appserver.1")<br />
oContext = oServer.GetObjectContext()<br />
The context object is used to commit or roll back<br />
transactions. Commit a transaction by calling the context<br />
object’s SetComplete() method:<br />
oContext.SetComplete()<br />
Roll back a transaction by calling the context object’s<br />
SetAbort() method:<br />
oContext.SetAbort()<br />
That’s it. That’s all you have to do to write a<br />
transactional component. Use the exact same technique<br />
for all transactional components with no modifications.<br />
How does it work? MTS creates a new transaction<br />
context for a new transactional component. In this case,<br />
GetObjectContext() returns a reference to the new<br />
transaction context. But if one transactional component<br />
creates an instance of another transactional component,<br />
then GetObjectContext() returns a reference to the original<br />
object’s context. How do you know which is which? You<br />
don’t. It doesn’t matter. If any method in a transaction<br />
calls the context object’s SetAbort() method, then the<br />
transaction will be rolled back. The component calling<br />
SetAbort() doesn’t need to know whether it has its own<br />
transaction or is part of another component’s transaction.<br />
The code runs the same either way.<br />
In the first two articles in this series, I created an<br />
account class with Deposit() and Withdraw() methods.<br />
Both called the Post() method, Deposit() passing the<br />
amount as a positive number, and Withdraw() passing it<br />
as a negative number. The Post() method fails if the new<br />
balance in the account is less than zero. Let me create<br />
a new, simplified Post() method using MTS. For the<br />
purposes of this simple example, assume I have a<br />
remote view of the account table. I also have three<br />
constants for return values: ERROR_SUCCESS,<br />
ERROR_OVERDRAWN, and ERROR_OTHER. First I get<br />
a reference to the object context and open the database<br />
and the remote view:<br />
LPARAMETERS nAccountNo, nAmount<br />
*-- Obtain a reference to transaction context.<br />
oServer = CREATEOBJECT("MTXaS.Appserver.1")<br />
oContext = oServer.GetObjectContext()<br />
*-- Open database and view.<br />
OPEN DATABASE Test<br />
USE Account<br />
In the next step, I add the amount of the transaction<br />
to the balance in the account. Note that in a withdrawal,<br />
nAmount would be a negative number:<br />
*-- Attempt to post the amount to the account.<br />
REPLACE balance WITH balance + nAmount<br />
Here’s where all the decisions are made:<br />
IF balance < 0<br />
*-- Insufficient funds, roll back.<br />
oContext.SetAbort()<br />
nSuccess = ERROR_OVERDRAWN<br />
ELSE<br />
*-- Sufficient funds, attempt to update.<br />
IF TABLEUPDATE(2, .T.)<br />
*-- Update successful.<br />
oContext.SetComplete()<br />
nSuccess = ERROR_SUCCESS<br />
ELSE<br />
*-- Update failed.<br />
oContext.SetAbort()<br />
nSuccess = ERROR_OTHER<br />
ENDIF<br />
ENDIF<br />
The first thing to check is whether or not there are<br />
sufficient funds for the transaction. If the balance is less<br />
than zero, the account would be overdrawn. Such a<br />
condition isn’t allowed, so I abort the transaction by<br />
calling SetAbort() and set the return value to my constant<br />
that indicates an overdraft.<br />
If no overdraft condition exists, I attempt to update<br />
the database with TABLEUPDATE(). If it succeeds, all is<br />
well and the transaction can be committed and the return<br />
value set to the constant indicating success. If the update<br />
fails, there’s some other problem. In this example, rather<br />
than dealing with the myriad reasons why an update<br />
could fail, I simply abort the transaction and set the return<br />
value to the constant indicating a problem.<br />
All that remains now is to clean up after the<br />
transaction and return the value:<br />
CLOSE DATABASES ALL<br />
RETURN nSuccess<br />
When the account component is added to an MTS<br />
package and used, the Post() method can be used to<br />
manage deposits and withdrawals. The Transaction<br />
Monitor in the Transaction Server Explorer can be used to<br />
track the number of transactions attempted along with the<br />
number of active transactions, the number of successful<br />
transactions, and the number of aborted transactions.<br />
Managing resources<br />
I see some hands going up right now—people who want<br />
to know why I open the database and the view without<br />
checking to see whether they’re already open and why I<br />
close everything at the end rather than leaving them open<br />
for future use. Good question.<br />
Without MTS, I’d have to write plenty of code to<br />
ensure that I’m not clashing with already-opened views. I<br />
also wouldn’t want to close everything as soon as I’m<br />
done with it. After all, opening a remote view requires<br />
that VFP load the ODBC manager, driver manager, and<br />
http://www.pinpub.com FoxTalk June 1998<br />
13
driver DLLs and open an ODBC connection to the<br />
database. Without MTS, I’d want to keep a connection<br />
open so that VFP doesn’t have to do all that work every<br />
time you call a method.<br />
MTS keeps DLLs in memory for a while after they’re<br />
no longer used. It also pools ODBC connections. If an<br />
object uses a connection, MTS keeps it open so that other<br />
objects can use the same connection. In this way, MTS<br />
can improve the performance of future requests for an<br />
object and can reduce the number of ODBC connections<br />
required. Remember that in client/server databases, more<br />
connections often means paying higher licensing fees and<br />
a higher requirement for resources.<br />
Transaction Server’s resource pooling, combined with<br />
Tahoe’s new apartment-model threading, provides some<br />
phenomenal performance improvements. I’ve conducted<br />
comparisons between components created and run in VFP<br />
5.0 and those created with Tahoe and run in Tahoe and<br />
MTS, and I’ve seen performance improvements of over<br />
100-fold when creating multiple instances of transactional<br />
components!<br />
To take advantage of resource pooling, I subclass all<br />
my transactional components from VFP Forms. Forms<br />
can have private datasessions so that multiple instances<br />
of in-process servers don’t step on the toes of other<br />
instances. If you do this, be sure you remember to make<br />
the appropriate environment settings. I can’t tell you how<br />
many times I’ve elevated my blood pressure by forgetting<br />
to SET EXCLUSIVE OFF in a component and received an<br />
OLE error (worse yet, with beta versions of these tools, I<br />
became an expert at causing GPFs). I’ll talk more about<br />
resource pooling later in this article.<br />
The promise comes true<br />
For several months now, I’ve been promising to show you<br />
how to easily reuse transactional components. MTS makes<br />
the promise come true. As I’ve shown with the account<br />
object, individual MTS transactional components never<br />
need to know their context. New components can be<br />
created that combine existing components into new<br />
transactions. The First National Bank of Dad will use an<br />
ATM component that can combine the Deposit() and<br />
Withdraw() methods of the existing account component to<br />
transfer allowance from one account to another with no<br />
modification to the account component.<br />
To do this, the Transfer() method of the ATM<br />
component uses the CreateInstance() method of its MTS<br />
context object to create each instance of the account<br />
component.<br />
*-- Obtain a reference to transaction context.<br />
oServer = CREATEOBJECT("MTXaS.Appserver.1")<br />
oContext = oServer.GetObjectContext()<br />
*-- Create an instance of account component.<br />
oAcct = oContext.CreateInstance("acct.account")<br />
When you create an instance of the account<br />
component with CreateInstance() instead of<br />
CREATEOBJECT(), you ensure that the account object<br />
inherits the transaction context of the calling object. The<br />
account component doesn’t need to know this, however.<br />
It knows how to get a reference to the transaction context;<br />
MTS takes care of the rest.<br />
Here’s what the Transfer() method of the ATM<br />
component looks like:<br />
*-- Obtain a reference to transaction context.<br />
oServer = CREATEOBJECT("MTXaS.Appserver.1")<br />
oContext = oServer.GetObjectContext()<br />
*-- Create an instance of account component.<br />
oAcct = oContext.CreateInstance("acct.account")<br />
nSuccess = oAcct.Deposit(nAccount1, nAmount)<br />
IF nSuccess = ERROR_SUCCESS<br />
*-- Create an instance of account component.<br />
oAcct = oContext.CreateInstance("acct.account")<br />
nSuccess = oAcct.Withdraw(nAccount2, nAmount)<br />
IF nSuccess = ERROR_SUCCESS<br />
*-- Deposit and withdrawal successful.<br />
oContext.SetComplete()<br />
ELSE<br />
*-- Withdrawal failed.<br />
oContext.SetAbort()<br />
ELSE<br />
*-- Deposit failed.<br />
oContext.SetAbort()<br />
ENDIF<br />
What’s going on here? First, remember that the<br />
Deposit() and Withdraw() methods of the account<br />
component simply call the Post() method after ensuring<br />
that the amount is positive for deposits and negative for<br />
withdrawals. Otherwise, the account component is being<br />
used unchanged in a new context. Notice that I deposit<br />
money in one account before withdrawing from the other.<br />
I did this deliberately to illustrate the point that it makes<br />
no difference what order components are used. If one<br />
method calls SetAbort(), then everything fails and is<br />
rolled back together. After running this code a few times,<br />
I firmly believe in magic.<br />
More on resources<br />
A few more hands have gone up. Why, you ask, did I<br />
create two instances of the account object—using the<br />
same reference memvar, no less—rather than reuse the<br />
same instance?<br />
One of the side effects of using a context object’s<br />
SetComplete() or SetAbort() method is that MTS will<br />
release the object as soon as the method calling<br />
SetComplete() or SetAbort() ends. This is what might be<br />
termed the MTS garbage collection. In the case of the<br />
account component, its resources are freed up as soon<br />
as the call to Deposit() or Withdraw() returns. The<br />
disadvantage to this is that you have to create another<br />
instance when you want to call another method. But<br />
there’s an advantage, too: MTS frees resources sooner,<br />
so they can be used again.<br />
Here’s an example of the account component’s<br />
GetBalance() method. Note that it makes no changes to<br />
the database, but calls SetComplete() anyway to ensure<br />
Continues on page 17<br />
14 FoxTalk June 1998<br />
http://www.pinpub.com
Use the Microsoft Scripting<br />
Control to Script with VFP<br />
Rod Paddock<br />
Have you ever wanted to add a programming language to<br />
your Visual FoxPro applications? If you answered yes, you’ve<br />
come to the right place. Microsoft has released an <strong>ActiveX</strong><br />
control for adding programming languages to your VFP<br />
applications—the Microsoft scripting control.<br />
VISUAL FoxPro supports two constructs for adding<br />
custom scripting to your applications. You can use<br />
Macro expansion—the & operator—or the EVAL()<br />
function. The problem with these constructs is they limit<br />
you to using simple commands and expressions. You can’t<br />
use control structures like IF/ENDIF, DO/LOOP, FOR/<br />
NEXT, or calling subroutines. This is where VFP’s native<br />
capabilities end and the advantages of the scripting<br />
control begins.<br />
Where to find the Microsoft scripting control<br />
The first step in using the Microsoft scripting control<br />
is to download and install it—it can be found at<br />
www.microsoft.com/scripting. Once you’ve installed the<br />
scripting control, add an instance of the control to a form,<br />
and get ready to rock.<br />
A quick note about a great utility . . .<br />
A utility program called CodeBlock by Randy Pearson<br />
was created a few years ago to address the limitation of<br />
combining macro expansion capabilities with looping<br />
constructs like DO WHILE and FOR/NEXT loops.<br />
Essentially, Randy built a runtime interpreter for FoxPro.<br />
With the exception of those commands that aren’t allowed<br />
outside the FoxPro IDE—such as COMPILE—just about<br />
any other block of code is allowed. The program allows<br />
you to store code in memo fields—and have that code<br />
executed on an interpreted basis—as if it were compiled.<br />
So if you have FoxPro or VFP applications that you’d like<br />
to equip with this capability, check out CodeBlock. A copy<br />
is included in this month’s Subscriber Downloads at<br />
www.pinpub.com/foxtalk.<br />
However, if you’re interested in adding new language<br />
capabilities like VBScript or JavaScript to your VFP<br />
applications, then read on. (Tip: If you’re interested in<br />
acquiring a VBScript reference, take a look at this URL:<br />
http://www.microsoft.com/scripting/default.htm?/<br />
FoxTalk<br />
scripting/vbscript/default.h. Or, if you already own<br />
Visual Studio, take a look at the Help files that come<br />
with Visual Interdev—it includes VBScript and<br />
JavaScript references.)<br />
Scripting control basics<br />
Using the scripting control in its basic form only requires<br />
four steps:<br />
1. Add an instance to a form.<br />
2. Set the language property. <strong>Your</strong> choices of languages<br />
are VBScript or JScript.<br />
3. Add the code you want to execute to the control using<br />
the AddCode method.<br />
4. Execute the code using the Execute Statement method<br />
of the control.<br />
The following code creates a basic message box<br />
function using VBScript:<br />
#DEFINE CRLF chr(10) + chr(13)<br />
*-- Set the language<br />
Thisform.oleScriptingControl.Language = "VBScript"<br />
*-- Add a function<br />
Thisform.oleScriptingControl.AddCode([Function Test] ;<br />
+ CRLF + [msgbox "hello"] ;<br />
+ CRLF + [ End Function ])<br />
*-- Execute the function<br />
Thisform.oleScriptingControl.ExecuteStatement("Test")<br />
That seems pretty straightforward, doesn’t it? Here’s<br />
how the code works: The first step to using the scripting<br />
control is to set the controls language property. You have<br />
two choices: VBScript and JScript. If you specify the<br />
language property as VBScript, the code you add with the<br />
AddCode method must be VBScript (a subset of Visual<br />
Basic for Applications). If you specify JScript, the code<br />
added must be in JavaScript.<br />
Next, use the AddCode method to load the control<br />
with source code to execute. The AddCode method<br />
accepts one parameter: a string that contains multiple<br />
lines of code delimited by CR+LF characters.<br />
Finally, run your code using either the Run or<br />
ExecuteStatement methods. The Run method executes<br />
functions or subroutines. The ExecuteStatement method<br />
http://www.pinpub.com FoxTalk June 1998<br />
15
executes a single statement (like “x = 1”) or a simple call<br />
to a function. There’s one aspect of this method that’s<br />
rather tedious—the requirement for you to code manually,<br />
delimiting each programming line with CR+LF sequences.<br />
As a result, wouldn’t it be convenient to have a<br />
mechanism similar to the VFP command window? I<br />
thought so, so I created a Scripting Control testbed.<br />
Figure 1 displays my Visual Basic command window<br />
style interface.<br />
Manipulating FoxPro objects with VBScript<br />
Now for something really cool. One of the most powerful<br />
aspects of the scripting control is its ability to manipulate<br />
VFP (or Visual Basic) native objects. Calling the<br />
AddObject method of the scripting control does this. Yes,<br />
the scripting control has a method called AddObject, so<br />
you can add an object to the scripting object container.<br />
The syntax for this method consists of two parameters.<br />
The first is the name that the object will be referenced by<br />
in your VBScript or JScript code. The second parameter is<br />
a handle to your object.<br />
To add a form to the scripting control, use the<br />
following syntax:<br />
thisform.oleScriptingControl.Object.AddObject( ;<br />
"frmFoxProForm",Thisform)<br />
One item you’ll quickly notice is the inclusion of the<br />
Object keyword. VFP provides this property to deal with<br />
<strong>ActiveX</strong> controls that share similar properties and<br />
methods with VFP objects. If you attempt to call the<br />
AddObject method without the Object keyword, VFP will<br />
address the FoxPro version of the AddObject method and<br />
not the AddObject method of the scripting control.<br />
Upon adding a reference to your VFP object, you can<br />
begin addressing the object in your VBScript using the<br />
first parameter of the AddObject method. The following<br />
code demonstrates changing properties on a VFP form<br />
Figure 1. This form demonstrates the capabilities of the Microsoft<br />
scripting control.<br />
using VBScript:<br />
Function SetFormProperties()<br />
If IsObject(frmFoxProForm) Then<br />
frmFoxProForm.Caption = "Hello World"<br />
frmFoxProForm.BackColor = 100000<br />
End If<br />
End Function<br />
This example demonstrates setting properties on a<br />
VFP object. You can also call VFP methods. The following<br />
code shows how to call the click event of a button on the<br />
scripting form:<br />
Function CallFormMethod()<br />
If IsObject(frmFoxProForm) Then<br />
frmFoxProForm.BackColor = 400000<br />
frmFoxProForm.cmdClickTest.Click()<br />
End If<br />
End Function<br />
Scripting “gotchas”<br />
When developing applications using the Microsoft<br />
scripting control, there are some “gotchas” you need to<br />
take into account. The first deals with adding VFP objects<br />
to the scripting control using the AddObject method. It’s<br />
important to make sure the scripting control releases your<br />
objects when it’s done with them. You can release your<br />
VFP controls by calling the scripting control’s Reset<br />
method. If you don’t use the Reset method, VFP might<br />
get a visit from Dr. Watson when you release the form<br />
with the scripting control on it. The best place to put the<br />
Reset call is in the QueryUnload event of the scripting<br />
control’s form.<br />
While I’m talking about Dr. Watson, there’s another<br />
item to take into account. If you use <strong>ActiveX</strong> controls in<br />
your applications, you need to turn FoxPro’s <strong>ActiveX</strong><br />
Dual Interface option off. You do this by putting the<br />
following call in either your main program or in the Load<br />
method of your form:<br />
*-- Turn <strong>ActiveX</strong> Dual Interface Option Off<br />
SYS(2333,0)<br />
If you don’t do this, FoxPro won’t release your<br />
forms correctly.<br />
Finally, for good measure, it’s always a good idea to<br />
call the scripting control’s Reset method after executing<br />
code. The reason for this is that the control has a tendency<br />
to become unstable if you call the AddCode method<br />
repeatedly.<br />
Conclusion<br />
Now that you have a basic understanding of using the<br />
scripting controls, you might say, “So what good does this<br />
do me?” Here are some uses I’ve already found handy:<br />
• Add your own macro language to your applications.<br />
(Hint: Use a memo field for storing code.)<br />
• Adopt existing Visual Basic or JavaScript code, like<br />
16 FoxTalk June 1998<br />
http://www.pinpub.com
functions stored in Active Server Pages.<br />
• Even use an alternate language for building your<br />
FoxPro applications.<br />
You’re only limited by your creativity when it comes<br />
to adding scripting to your applications.<br />
Finally, I’d like to thank two people for assisting me<br />
with this article: Ken Levy, for inspiring it in the first<br />
place, and John Petersen, for making sure my article<br />
Transactions: Tahoe and MTS . . .<br />
Continued from page 14<br />
that resources get released:<br />
*-- Obtain a reference to transaction context.<br />
oServer = CREATEOBJECT("MTXaS.Appserver.1")<br />
oContext = oServer.GetObjectContext()<br />
*-- Open database and view.<br />
OPEN DATABASE Test<br />
USE Account<br />
*-- Check the balance.<br />
nBalance = account.balance<br />
*-- Complete transaction.<br />
oContext.SetComplete()<br />
CLOSE DATABASES ALL<br />
RETURN nBalance<br />
“Gotchas” galore<br />
Those of you who attend the 1998 Visual FoxPro DevCon<br />
might notice that my beard is a little grayer this year than<br />
last. Working with new features of beta software when<br />
there’s nobody to go to with questions sometimes seems<br />
to take many years off your life. Furthermore, in a<br />
distributed environment such as I’ve described, it can be<br />
tough to know which product’s documentation to refer to<br />
when you get an error. I hope you’ll benefit from my own<br />
accelerated aging process.<br />
MTS components can have no user interface.<br />
They can’t use commands like WAIT WINDOW or<br />
MESSAGEBOX(). Nor can ODBC connections open<br />
ODBC login screens. When you create connections for<br />
remote views, be certain to use DBSETPROP() to set the<br />
connection’s DispLogin property to 3 to ensure that the<br />
user is never prompted.<br />
MTS components are always in-process servers. In the<br />
past, there was no difference in VFP between multi-use inprocess<br />
servers and single-use in-process servers. Tahoe<br />
OLEPUBLIC classes that will be used in MTS must be set<br />
for multi-use instancing.<br />
Debugging servers running in MTS can be a little<br />
tricky to debug. The process goes something like this:<br />
Create DLL, create MTS package, run code, crash. So then<br />
would be useful for other VFP developers. ▲<br />
06PADDOC.ZIP at www.pinpub.com/foxtalk<br />
Rod Paddock is president of Dash Point Software, Inc., which is<br />
based in Seattle, and specializes in developing applications using Visual<br />
Interdev, VFP, VB, and SQL Server. DPSI was a finalist in the 1998 Visual<br />
FoxPro Excellence Awards. Rod is currently working with John Petersen<br />
on his new book, Visual FoxPro 6 Enterprise Development<br />
(Prima Publishing). www.dashpoint.com, rpaddock@dashpoint.com.<br />
you make a change in your code and attempt to rebuild<br />
the DLL. But the DLL is in use and can’t be overwritten.<br />
The problem is that MTS keeps components in memory<br />
for faster reuse. In the Transaction Server Explorer, you<br />
can select the correct computer, right-click, and choose the<br />
Shutdown Server Processes option. If you try to build the<br />
DLL again and get the same error, you might want to<br />
resort to using the Task Manager to end the processes<br />
instead. Open the Task Manager and look for “MTX.EXE”<br />
on the Processes page. You might see it in the list more<br />
than once. Select it and click the End Process button.<br />
Don’t try this except on development machines.<br />
Don’t forget that your in-process servers aren’t<br />
running in your application’s process, they’re running in<br />
an MTS process. Any environment settings you’ve made<br />
in your application don’t apply in the MTS process. I aged<br />
seven years just because of a file-in-use error due to my<br />
failure to remember to SET EXCLUSIVE OFF.<br />
More to come<br />
No, I’m not planning another article in this series, but be<br />
sure to look for Jim Falino’s upcoming how-to article that<br />
discusses the specific implementation tricks and tips.<br />
I’m very excited about the possibilities presented by<br />
Tahoe and MTS. Although there’s much room for<br />
improvement, the combination offers many advantages<br />
to client/server application developers today. And<br />
when SQL Server 7 is available later this year with its<br />
embeddable SQL Desktop, those advantages will be<br />
available for all applications. There will be nothing to stop<br />
me from delivering the most powerful, robust database<br />
applications to all my clients, whether they have a<br />
hundred workstations or one laptop. ▲<br />
Gary DeWitt is software magician at Sunpro, Inc., the leader in fire service<br />
software. He lives on the California shore of Lake Tahoe and is convinced<br />
the next version of Microsoft Visual FoxPro, code-named Tahoe, was<br />
named with him in mind. While Visual FoxPro is his favorite development<br />
tool, he also programs in C++, Java, and Visual Basic. Gary is a Microsoft<br />
Certified Professional and Microsoft Most Valuable Professional.<br />
gdewitt@sierra.net.<br />
http://www.pinpub.com FoxTalk June 1998<br />
17
Synchronized Swimming<br />
Paul Maskens and Andy Kramek<br />
This month, Paul and Andy show how to make multiple forms<br />
with private Datasessions navigate to the same record.<br />
Andy: Here’s a nice little problem, Paul. I’ve got a form<br />
(running in a private DataSession) that’s used to display<br />
saved Visual FoxPro forum messages, and obviously I<br />
want to be able to search the underlying table to find a<br />
particular message. This search might be by Subject, by<br />
Author Name, by Thread number, and so on. I just don’t<br />
know which will be needed at any particular moment. I<br />
could simply use a popup form with, say, a grid on it,<br />
select the message I want, pass its record number back to<br />
the main form, and then do a LOCATE and REFRESH<br />
there. However, it seemed to me that linking the two<br />
forms in some way would be a cleaner (and, more<br />
importantly, more generic) approach.<br />
My first thought was to have the forms share a<br />
common DataSession—after all, that’s pretty easy to do<br />
when the child form is modal. You just set its DataSession<br />
to 1 (Default), and it will inherit its DataSession from the<br />
parent form.<br />
Paul: Yeah, but that’s undocumented behavior, isn’t it?<br />
Can we really rely on it? And doesn’t it also cause<br />
some problems with DataSession names? Despite the<br />
DataSession name getting lost, this technique worked in<br />
VFP 3. But I’ll have to retest it in 5.0 and 6.0 if I ever<br />
recompile that application.<br />
Andy: Well, it’s true that VFP does seem to lose track of<br />
the DataSession name upon returning from a child form<br />
set up in this way—it just shows “Unknown” as the<br />
DataSession name. But it doesn’t seem to affect anything.<br />
As for relying on behavior that’s undocumented—you<br />
have a point. Remember—back in 3.0—the “Empty” base<br />
class that’s used when you do a “SCATTER NAME”? That<br />
was undocumented and exposed in VFP 3.0, but it<br />
disappeared without warning in 3.0b!<br />
On second thought, better to make the child form<br />
switch itself to the parent form’s DataSession. We can do<br />
that by passing the parent form’s DataSessionID property<br />
to the child form like this:<br />
DO FORM frmChild WITH ThisForm.DataSessionID<br />
In the receiving form, just set the DataSessionID property<br />
in the Init() method, like this:<br />
LPARAMETERS tnDSID<br />
ThisForm.DataSessionID = tnDSID<br />
The Kit Box FoxTalk<br />
Paul: Oh, great! But what happened to my bound controls<br />
when I did that? After all, they’ve already been initialized<br />
in the child form’s DataSession—now I’m switching it! Is<br />
that what made all my controls lose their controlsources?<br />
Andy: Hmm. A good point. Any bound controls will<br />
irrevocably lose their binding. I suppose we could reset<br />
them all in the Init() (or a custom method called by<br />
the Init())?<br />
Paul: Yes, that would work. However, I prefer a method<br />
called by the Init, although this would be a bit of a pain if<br />
you had a pageframe with 40 controls on each of three<br />
pages! I suppose you could go through the .controls[]<br />
collection and reset the controlsource properties, but there<br />
has to be a better way than recursion into every container<br />
you have on your form.<br />
Andy: Okay, wise guy—what do you suggest, then?<br />
Paul: Pass the Primary Key of the current record between<br />
the forms! However, instead of diving in and creating my<br />
own methods to send and receive the Primary Key, I<br />
cheated. I used Drag and Drop to link two forms!<br />
On the “sending” form, I had a command<br />
button that assigned the table’s Primary Key (well,<br />
ALLTRIM(STR(ID), actually) to its caption as I moved the<br />
mouse over it. Then I set the DragMode of the button to<br />
Automatic in the Properties window.<br />
The “receiving” form looked at the caption of the<br />
object it received in its DragDrop method and then did a<br />
SEEK() on VAL() of that object’s caption.<br />
So I had two forms that were sort of loosely coupled.<br />
The receiving form could navigate separately because it<br />
used a private DataSession. The sending form would<br />
always navigate separately, again with a private<br />
DataSession. Just drag the button from the sending<br />
form to the receiving form—voila! Record pointers<br />
synchronized . . .<br />
Andy: That’s pretty neat! It does depend on user<br />
intervention, though, doesn’t it? I mean, what happens if<br />
they close the child form without dragging the button to<br />
it? Not much, I suppose?<br />
Paul: Absolutely right! You see, I intended this mechansim<br />
to be a way of linking an overview to a detail form when<br />
required, not permanently. You got me thinking, though.<br />
18 FoxTalk June 1998<br />
http://www.pinpub.com
It’s only a moment’s work to add a “LINK” check box,<br />
and another moment to use Refresh() to set the cmdLink<br />
caption instead of waiting for the mouse. Refresh() is<br />
called after record navigation to update the contents of all<br />
the controls. Next, add a test in the sending form’s<br />
Refresh() like so:<br />
IF chkLink.value = .t.<br />
Form2.DragDrop(THISFORM.cmdLink).<br />
ENDIF<br />
Andy: Whoa! Slow down a bit there, you’ve lost me.<br />
Explain how these two forms link themselves again—<br />
how does one form “know” about the other one?<br />
Paul: Okay, I know the name of Form2, right? So I can<br />
loop through the _SCREEN.Forms[] collection looking for<br />
the Name. I could have used the Caption property, or<br />
Comment, or Tag—Name seems easiest.<br />
When I find it, I can fake a DragDrop event dropping<br />
on Form2 by calling the DragDrop() method with<br />
parameters that look like a real DragDrop has happened.<br />
You know that events happen. What VFP does when<br />
an event happens is to run the code in the method with<br />
the same name to handle any special actions when the<br />
event happens. So if I call that method directly, the<br />
program can’t tell the difference.<br />
I set the command button’s caption, then call this:<br />
_SCREEN.Forms[i].DragDrop(THISFORM.cmdDrag,0,0)<br />
to fool VFP into running my navigation code in the form.<br />
LPARAMETERS oSource, nXCoord, nYCoord<br />
SELECT (THISFORM.MainTable)<br />
lnOldRec = RECNO()<br />
SEEK VAL(oSource.caption)<br />
ENDIF<br />
I made it a little more complex, because my program<br />
fakes DragDrop with coordinates of 0,0, and I assume<br />
anything other than that is a real drop. So the focus should<br />
be set to THISFORM—Form2.<br />
Without any extra custom methods and with only one<br />
object (perhaps an invisible Label), you could link two<br />
forms together. Yes, before you say it, I think properly<br />
named methods are a good idea. I’m overloading<br />
DragDrop to make it do something it isn’t meant to do.<br />
However, a better way is to have a set of interacting<br />
objects in a Mediator-Colleague pattern.<br />
Andy: Huh? What does that mean?<br />
Paul: The Mediator receives messages and sends them to<br />
every Colleague it knows about, except the sender. The<br />
Colleague registers with a Mediator and processes any<br />
message it receives.<br />
Andy: Ah, I see. So in our example, the parent form would<br />
originate a message and send it to the Mediator object,<br />
which in turn would simply pass it on to the objects it<br />
knows about. The child form is then responsible for<br />
registering with the Mediator so that it can receive<br />
(and presumably send) messages by that route. That’s<br />
pretty cool!<br />
Paul: Right (or possibly wrong ). In your Application<br />
object, you’d need to have a Register(toColleague)<br />
method where other objects sign up to receive messages.<br />
Any object that wants to receive messages has to have a<br />
HandleMessage() method.<br />
Andy: Hang on there, fellah—this all sounds a bit specific.<br />
I don’t use an Application object all the time. Wouldn’t<br />
it be better if you made your Mediator a class in its<br />
own right?<br />
Paul: I think so. This would mean all the pointers to the<br />
registered objects and the code that passes messages to<br />
them would be encapsulated in an object. That’s proper<br />
OOP. For now, though, I’d like to keep it simple and<br />
assume your Application object exists and is called<br />
goApp.<br />
Andy: Okay, I’ll buy that for now. So how does it all work<br />
in practice?<br />
Paul: Easy. In the Init(), your form class has a call to<br />
goApp.Register(THIS). The Application object’s<br />
Register(toColleague) method stores the object pointer<br />
in a 2-D array, along with SYS(1272,toColleague). That<br />
SYS() function gets the containership hierarchy and<br />
the name of the object—which will be needed later!<br />
So, to recap: goApp.aColleague[i,1] is the object,<br />
goApp.aColleague[i,2] is its name.<br />
<strong>Your</strong> Application object should also have a<br />
Mediate(tcMessage, toSender) method. This loops<br />
through the stored object array (why use i? It’s a sort<br />
of tradition ) and checks—using the<br />
SYS(1272,toSender)—to see whether the entry in<br />
.aColleague[i,2] matches. If it does, ignore it—you<br />
generally don’t want to send the message back<br />
to the sender. Otherwise, call the<br />
.aColleague[i,1].HandleMessage(tcMessage).<br />
That’s it!<br />
Andy: Aha! I get it now. That SYS(1272) call is working<br />
around the fact that we can’t compare objects with each<br />
other. So my Form class has to have an extra method<br />
HandleMessage() and a call (presumably conditional) in<br />
its Init() to the mediator Register(), like this:<br />
PROCEDURE INIT<br />
IF TYPE( 'goApp') = "O" AND ! ISNULL( goApp )<br />
goApp.Register( THIS )<br />
ENDIF<br />
But what goes into the HandleMessage() method?<br />
Paul: That depends on what messages you want the object<br />
to react to. Of course, you need to define some<br />
standards—like perhaps “SEEK:“.<br />
http://www.pinpub.com FoxTalk June 1998<br />
19
Then you can just use a CASE statement in the<br />
HandleMessage() method to define which messages to<br />
recognize:<br />
DO CASE<br />
CASE tcMessage = "SEEK:"<br />
*** React to this message<br />
lnPK = VAL( SUBSTR( tcMessage, 6 ))<br />
SEEK lnPK<br />
OTHERWISE<br />
*** Do nothing - ignore the message<br />
ENDCASE<br />
Andy: Just one thought. Haven’t we now got an external<br />
reference to an object in the Mediator? Doesn’t this mean<br />
that we can no longer release the object?<br />
Paul: Correct! So we also need an UnRegister() method<br />
that’s called from each Form’s Unload() method. Like<br />
Register(), goApp.UnRegister(THIS) would suffice.<br />
All it needs is to loop through .aColleague[i,1] again,<br />
testing SYS(1272, toSender) as before, but this time, on<br />
finding a match, just set .aColleague[I,2] to .NULL. so that<br />
the object reference is discarded.<br />
Oh, and about your desire for reusable classes? Here’s<br />
one I prepared earlier, in the best Blue Peter tradition . . .<br />
Class cusMediator is the Mediator half of the pattern.<br />
This can either be added to a form at design time, in<br />
which case it should be named “oMediator”, or it can be<br />
created as an uncontained object, in which case it should<br />
be referred to by a global variable goMediator.<br />
**************************************************<br />
*-- Class: cusMediator<br />
*<br />
#DEFINE _CR CHR(10)<br />
*<br />
DEFINE CLASS cusMediator AS custom<br />
*-- Number of registered colleague controls<br />
nColleagues = 0<br />
Name = "cusMediator"<br />
*-- Array of colleague control Hierarchy, oRef.<br />
DIMENSION acolleagues[1,2]<br />
*-- Called by colleague to register itself with<br />
*-- the Mediator<br />
PROCEDURE register<br />
LPARAMETERS toColleague<br />
WITH THIS<br />
.nColleagues = .nColleagues +1<br />
DIMENSION .aColleagues[ .nColleagues, 2]<br />
.aColleagues[ .nColleagues, 1] ;<br />
= SYS( 1272, toColleague )<br />
.aColleagues[ .nColleagues, 2] = toColleague<br />
ENDWITH<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT THIS.Name +" register()" +_CR ;<br />
+SYS( 1272, toColleague ) +_CR ;<br />
ELSE<br />
WAIT WINDOW THIS.Name +" register()" +_CR ;<br />
+SYS( 1272, toColleague ) +_CR ;<br />
TIMEOUT 2<br />
ENDIF<br />
ENDIF<br />
ENDPROC<br />
*-- Removes all object references to colleagues,<br />
*-- call before the Destroy() to prevent GPFs<br />
PROCEDURE cleanup<br />
LOCAL lnCount<br />
WITH THIS<br />
FOR lnCount = 1 TO .nColleagues<br />
.aColleagues[ lnCount, 1] = ""<br />
.aColleagues[ lnCount, 2] = .NULL.<br />
NEXT<br />
.nColleagues = 0<br />
ENDWITH<br />
ENDPROC<br />
*-- Receives Source object and message to<br />
*-- mediate (String)<br />
PROCEDURE mediate<br />
LPARAMETERS toColleague, tcMessage<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT THIS.Name +" mediate()" +_CR ;<br />
+tcMessage +_CR ;<br />
+SYS( 1272, toColleague )<br />
ELSE<br />
WAIT WINDOW THIS.Name +" mediate()" +_CR ;<br />
+tcMessage +_CR ;<br />
+SYS( 1272, toColleague );<br />
TIMEOUT 2<br />
ENDIF<br />
ENDIF<br />
RETURN THIS.SendMessage( toColleague, tcMessage )<br />
ENDPROC<br />
*-- Sends the message received by Mediate() to all<br />
*-- the colleagues registered EXCEPT the one<br />
*-- that sent it<br />
PROCEDURE sendmessage<br />
LPARAMETERS toColleague, tcMessage<br />
LOCAL lcColleague, lnColleague, llStatus<br />
WITH THIS<br />
lcColleague = SYS( 1272, toColleague )<br />
llStatus = .T.<br />
FOR lnColleague = 1 to .nColleagues<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT THIS.Name ;<br />
+"sendmessage("+allt(str(lnColleague))+")";<br />
+_CR ;<br />
+.aColleagues[ lnColleague, 1]<br />
ELSE<br />
WAIT WINDOW THIS.Name ;<br />
+"sendmessage("+allt(str(lnColleague))+")";<br />
+_CR ;<br />
+.aColleagues[ lnColleague, 1] ;<br />
TIMEOUT 2<br />
ENDIF<br />
ENDIF<br />
IF NOT( .aColleagues[ lnColleague, 1] ;<br />
== lcColleague )<br />
IF .aColleagues[lnColleague,2] ;<br />
.Receive(tcMessage)<br />
*<br />
* Do nothing, assumes all colleagues receive OK<br />
*<br />
ELSE<br />
*<br />
* Note colleague failed to handle message<br />
*<br />
llStatus = .F.<br />
ENDIF<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT THIS.Name ;<br />
+" sendmessage()" +_CR ;<br />
+.aColleagues[ lnColleague, 1] +_CR ;<br />
+IIF(llStatus,"OK","Fail")<br />
ELSE<br />
WAIT WINDOW ;<br />
THIS.Name +" sendmessage()" +_CR ;<br />
+.aColleagues[ lnColleague, 1] +_CR ;<br />
+IIF(llStatus,"OK","Fail") ;<br />
TIMEOUT 2<br />
ENDIF<br />
ENDIF<br />
NEXT<br />
ENDWITH<br />
20 FoxTalk June 1998<br />
http://www.pinpub.com<br />
*
* If any colleague failed to handle the message,<br />
* the status will be .F. and not .T. as set<br />
* earlier<br />
*<br />
RETURN llStatus<br />
ENDPROC<br />
PROCEDURE unregister<br />
LPARAMETERS toColleague<br />
LOCAL lnColleague, llRetVal<br />
WITH THIS<br />
lnColleague = ASCAN( .aColleagues, ;<br />
SYS( 1272, toColleague ))<br />
IF lnColleague != 0<br />
lnColleague = ASUBSCRIPT( .aColleagues, ;<br />
lnColleague, 1 )<br />
llRetVal = .t.<br />
.aColleagues[ lnColleague, 1] = ""<br />
.aColleagues[ lnColleague, 2] = .NULL.<br />
= ADEL( .aColleagues, lnColleague )<br />
DIMENSION .aColleagues[ .nColleagues, 2 ]<br />
ELSE<br />
llRetVal = .f.<br />
ENDIF<br />
ENDWITH<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT THIS.Name +" UnRegister()" +_CR ;<br />
+SYS( 1272, toColleague ) +_CR ;<br />
ELSE<br />
WAIT WINDOW THIS.Name +" UnRegister()" +_CR ;<br />
+SYS( 1272, toColleague ) +_CR ;<br />
TIMEOUT 2<br />
ENDIF<br />
ENDIF<br />
RETURN llRetVal<br />
ENDPROC<br />
ENDDEFINE<br />
*<br />
*-- EndDefine: cusMediator<br />
**************************************************<br />
Class cusColleague is the Colleague half of the<br />
pattern. This class can be added to any container, in which<br />
case the DoAction() method will probably do things to<br />
THIS.parent—I usually just call a method. I expect that<br />
the DoAction() method will be overridden in any instance<br />
or subclass.<br />
You could make it collaborate with any object, with a<br />
little modification—like passing THIS to the Init(toClient)<br />
method and storing that in a property of the Colleague for<br />
use in the DoAction() method.<br />
**************************************************<br />
*-- Class: cusColleague<br />
*<br />
#DEFINE _CR CHR(10)<br />
*<br />
DEFINE CLASS cusColleague AS Custom<br />
Name = "cusColleague"<br />
*-- Message is sent here by mediator, this passes<br />
*-- it on to DoAction() and returns the result.<br />
PROCEDURE Receive<br />
LPARAMETERS tcMessage<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT SYS(1272, THIS) +" receive()" +_CR ;<br />
+ tcMessage<br />
ELSE<br />
WAIT WINDOW ;<br />
SYS(1272, THIS) +" receive()" +_CR ;<br />
+tcMessage ;<br />
TIMEOUT 2<br />
ENDIF<br />
ENDIF<br />
RETURN THIS.DoAction( tcMessage )<br />
ENDPROC<br />
*-- Performs anything necessary to handle the<br />
*-- incoming message. Returns success/fail.<br />
PROCEDURE DoAction<br />
LPARAMETERS tcMessage<br />
*<br />
* This will of course be overridden in an instance<br />
* of this class!<br />
*<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT SYS(1272, THIS) +CHR(13) ;<br />
+"DoAction( "+ tcMessage +" )"<br />
ELSE<br />
WAIT WINDOW SYS(1272, THIS) +CHR(13) ;<br />
+"DoAction( "+ tcMessage +" )" ;<br />
TIMEOUT 2<br />
ENDIF<br />
RETURN<br />
ENDPROC<br />
*-- Given a message (String) it sends it off to<br />
*-- the mediator referenced through<br />
*-- THISFORM.oMediator.Mediate()<br />
PROCEDURE send<br />
LPARAMETERS tcMessage<br />
*<br />
* Send message via form mediator and return.<br />
*<br />
IF TYPE("THISFORM.oMediator") != "U"<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT SYS(1272, THIS) +" send()" +_CR ;<br />
+tcMessage +_CR ;<br />
+"Form Mediator"<br />
ELSE<br />
WAIT WINDOW ;<br />
SYS(1272, THIS) +" send()" +_CR ;<br />
+tcMessage +_CR ;<br />
+"Form Mediator";<br />
TIMEOUT 2<br />
ENDIF<br />
ENDIF<br />
RETURN THISFORM.oMediator.Mediate( ;<br />
THIS, tcMessage )<br />
ENDIF<br />
* Unless there isn't one!<br />
* Send message via global mediator and return.<br />
*<br />
IF TYPE("goMediator") != "U"<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT SYS(1272, THIS) +" send()" +_CR ;<br />
+tcMessage +_CR ;<br />
+"Global Mediator"<br />
ELSE<br />
WAIT WINDOW THIS.Name +" send()" +_CR ;<br />
+tcMessage +_CR ;<br />
+"Global Mediator";<br />
TIMEOUT 2<br />
ENDIF<br />
ENDIF<br />
RETURN goMediator.Mediate( THIS, tcMessage )<br />
ENDIF<br />
*<br />
* No mediators on standard interfaces, fail.<br />
*<br />
IF TYPE("glDebug") != "U"<br />
IF "Visual FoxPro 05" $ VERS()<br />
DEBUGOUT SYS(1272, THIS) +" send()" +_CR ;<br />
+tcMessage +_CR ;<br />
+"NO Mediator - FAILED"<br />
ELSE<br />
WAIT WINDOW THIS.Name +" send()" +_CR ;<br />
+tcMessage +_CR ;<br />
+"NO Mediator - FAILED";<br />
TIMEOUT 2<br />
ENDIF<br />
http://www.pinpub.com FoxTalk June 1998<br />
21
ENDIF<br />
RETURN .F.<br />
ENDPROC<br />
PROCEDURE Init<br />
*<br />
* Register with form mediator and return.<br />
*<br />
IF TYPE("THISFORM.oMediator") != "U"<br />
RETURN THISFORM.oMediator.Register( THIS, .F. )<br />
ENDIF<br />
* Unless there isn't one!<br />
* Register with global mediator and return.<br />
*<br />
IF TYPE("goMediator") != "U"<br />
RETURN goMediator.Register( THIS, .F. )<br />
ENDIF<br />
*<br />
* No mediators on standard interfaces, fail.<br />
* In failing this will prevent FORM<br />
* instantiation under VFP 3, but this object<br />
* will merely not exist under VFP 5<br />
*<br />
RETURN .F.<br />
ENDPROC<br />
PROCEDURE Destroy<br />
*<br />
* Remove from form mediator and return.<br />
*<br />
IF TYPE("THISFORM.oMediator") != "U"<br />
RETURN THISFORM.oMediator.UnRegister( THIS )<br />
ENDIF<br />
* Unless there isn't one!<br />
* Remove from global mediator and return.<br />
*<br />
IF TYPE("goMediator") != "U"<br />
RETURN goMediator.UnRegister( THIS )<br />
ENDIF<br />
*<br />
* No mediators on standard interfaces, fail.<br />
*<br />
Editorial: Enter Sandman . . .<br />
Continued from page 2<br />
so let’s do it. I mean, wouldn’t it be cooooool?” Who gives<br />
a rip if it should be done? They’ve got the business in a<br />
perpetual death grip of one-upmanship. Their processor<br />
runs at 410 MHz. Let’s make ours run at 450—and we’ll<br />
price it lower!<br />
I’m not saying I want to go back to dBASE II. We’ve<br />
had some wonderful advances that are a joy to work<br />
with. Visual FoxPro is still a thrill a minute (including the<br />
thrills you get when Dr. Watson comes to visit ).<br />
And the rumor is that Office will be using a single<br />
code-base worldwide next year. That’ll be a nice change,<br />
don’t you think?<br />
It’s the same in the music industry. If you’re going to<br />
follow bands these days, you have to be nimble. This<br />
month’s hot news is next month’s history. I’m reminded<br />
of the scene in the movie “Clueless,” where several<br />
teenage girls are checking out a jacket in the mall. One<br />
derides the jacket as “so five minutes ago.” The other<br />
decides not to buy. Fashionable today, boring tomorrow.<br />
RETURN .F.<br />
ENDPROC<br />
ENDDEFINE<br />
*<br />
*-- EndDefine: cusColleague<br />
**************************************************<br />
Paul: I’ve left it in my debugging code, which I control<br />
using the existence of the global variable glDebug. This is<br />
a hangover from VFP 3, where we had no debug output<br />
window, yet I wanted to control whether or not my debug<br />
output appeared. It’s not defined because I’m not usually<br />
debugging this code. If you want to see those messages,<br />
simply type “glDebug = .t.” and press Enter in the<br />
Command window.<br />
You’ll notice that I’ve adopted a belt-and-braces<br />
approach in the Colleague class. I’ve checked for both a<br />
global mediator object (goMediator) and a mediator object<br />
added to the current form (THISFORM.oMediator)<br />
because I wasn’t sure how it might be used. I haven’t<br />
added a third test for goApp.oMediator because I believe<br />
in using separate public variables for various managers.<br />
Practical OOP here, Andy.<br />
Andy: I’m speechless with admiration! ▲<br />
Paul Maskens is a VFP product specialist who works for the Visual<br />
Development Studio of Chelmsford, based in Oxford, England.<br />
pmaskens@compuserve.com.<br />
Andy Kramek is a long-time FoxPro developer, independent<br />
contractor, and occasional author based in Birmingham, England.<br />
andykr@solihull.bytenet.co.uk.<br />
So you’re going to have to be just as nimble to follow<br />
the software trends. Yesterday’s big news is today’s<br />
yawn. Don’t you feel a little cheated if you’ve made a big<br />
investment in last year’s alphabet soup of database access<br />
acronyms, and then did it again this year, only to find out<br />
it doesn’t all really work yet, and you’re going to have to<br />
do the learning curve again next year?<br />
Where I’m heading with this, though, is that the next<br />
couple of years are going to be decision time for a lot of<br />
us. People in my user group complain because we want to<br />
send out meeting notices via e-mail. “Sure, I have e-mail.<br />
But I only check it about once a month.” C’mon! As a<br />
friend of mine said (and as I’ve echoed in this column<br />
before), “If you’re not willing to make a professional<br />
investment in your career here, then run, don’t walk, to<br />
the exit of this industry.”<br />
I’m tired. Time to go home. I’m going to pull a cold<br />
one from the fridge, turn off all the lights, and listen to<br />
one of the all-time greats in heavy metal. Classics never<br />
go out of style, and neither will “Enter Sandman.” I’ll<br />
have more energy tomorrow. I’ll need it, to stay up with<br />
these young whippersnappers . . . ▲<br />
22 FoxTalk June 1998<br />
http://www.pinpub.com
Traversing Transactions . . .<br />
Continued from page 11<br />
IF NOT TableUpdate( … )<br />
llRollBack = .T.<br />
ELSE<br />
IF NOT TableUpdate( … )<br />
llRollBack = .T.<br />
ENDIF<br />
ENDIF<br />
ENDIF<br />
IF llRollBack<br />
ROLLBACK<br />
ELSE<br />
ENDTRANSACTION<br />
ENDIF<br />
The preceding code is pseudo-code, so don’t take it<br />
literally. Here I used a series of nested IF statements to<br />
control the actions. I could have put all of the aliases in an<br />
array and used a FOR/ENDFOR loop to go through them,<br />
exiting the loop when they were all done or when any one<br />
of them failed.<br />
What can go wrong?<br />
In the preceding pseudo-code, assume that the transaction<br />
failed and that you issued the ROLLBACK. What, exactly,<br />
do you roll back to? Are the buffers returned to their<br />
pre-edit state, or are the buffers still dirty with the<br />
user’s edits?<br />
The answer is that the buffers are restored to the<br />
dirty state that they were in prior to the BEGIN<br />
TRANSACTION. This means that you still have to deal<br />
with the user’s edits. You have the control here—you can<br />
put the user back into the form and let him or her decide<br />
what to do, or you can TableRevert all the aliases and<br />
discard the user’s work. This is all up to you in the code<br />
that you write.<br />
What happens if the computer shuts off during the<br />
transaction? Obviously, VFP has no record of the<br />
transaction when the system is restarted. So did some of<br />
the tables get updated or not? None of the tables got<br />
updated, none of the buffers are dirty, and the result is<br />
just as if a ROLLBACK had been executed.<br />
Another situation that’s occurred and caused a<br />
<strong>Tame</strong> <strong>Your</strong> <strong>ActiveX</strong> <strong>Controls</strong> . . .<br />
Continued from page 5<br />
acts as the placeholder for the TreeView control. The<br />
cNewObjectName properties for oTreeViewLoader<br />
and oImageListLoader specify the names of the controls<br />
to create (oTree and oImageList, respectively).<br />
oTreeViewLoader also has the name of the placeholder<br />
object (shpTreeView) in its cObjectName property and<br />
the name of the TreeView subclass we want to use<br />
(MyTreeView) and its location (ACLASSES.PRG) in the<br />
cClass and cLibrary properties. The Init method of the<br />
form sets some properties for the <strong>ActiveX</strong> controls,<br />
loading images in the case of the ImageList and nodes in<br />
the case of the TreeView.<br />
lot of problems is when the developer puts together a<br />
form and puts a BEGIN TRANSACTION in the Init and<br />
issues either an ENDTRANSACTION or a ROLLBACK<br />
in the destroy, depending on which button the user<br />
chose to exit the form. What problems could this cause?<br />
The first is that no other user can do anything with any<br />
of the tables involved in this form until the current user<br />
exits the form and releases the locks. I’ve seen this<br />
situation used as an argument against transactions—well,<br />
come on now, anything that’s used incorrectly can cause<br />
problems. Transactions used correctly will limit the<br />
time between the beginning and ending to the smallest<br />
period possible. The Init to Destroy of a form is<br />
certainly not the smallest time period possible—it’s<br />
a time period that’s not even under the control of<br />
the developer.<br />
It’s been said that VFP’s transactions aren’t as<br />
“robust” as the transaction handling of client/server<br />
database servers. I say, so what? Is the fact that a semi<br />
tractor has brakes that are more “robust” than the brakes<br />
on my car a reason to not use my car’s brakes? No, it’s<br />
not. VFP’s transactions are helpful, and they serve a<br />
purpose. They should be used, even when the data is<br />
stored in a client/server database.<br />
Conclusion<br />
Transactions in VFP are a very valuable feature of the<br />
product. They allow you to group a number of update<br />
operations into one “all or none” operation. In today’s<br />
world, you seldom build forms that only manage a single<br />
table—the multi-table form has become the norm.<br />
Managing these multi-table updates becomes a very<br />
important aspect of your work, and transactions are<br />
invaluable in doing this. ▲<br />
Jim Booth is a Visual FoxPro developer and trainer. He has spoken<br />
at FoxPro conferences in North America and Europe. Jim has been<br />
a recipient of the Microsoft Most Valuable Professional Award<br />
every year since it was first presented in 1993. 203-758-6942,<br />
72130.2570@compuserve.com.<br />
local loPicture<br />
* Load the ImageList images.<br />
with This.oImageList<br />
loPicture = loadpicture('AUDIO.ICO')<br />
.ListImages.Add(, 'Audio', loPicture)<br />
loPicture = loadpicture('DESKTOP.ICO')<br />
.ListImages.Add(, 'Desktop', loPicture)<br />
endwith<br />
* Set some TreeView properties.<br />
with This.oTree<br />
.ImageList = This.oImageList.Object<br />
.Style = 7<br />
.LineStyle = 1<br />
.LabelEdit = 1<br />
.HideSelection = .F.<br />
.Indentation = 25<br />
* Load the TreeView with sample nodes.<br />
.Nodes.Add(, 1, 'Top1', 'First Top Node', 'Audio')<br />
http://www.pinpub.com FoxTalk June 1998<br />
23
.Nodes.Add(, 1, 'Top2', 'Second Top Node', 'Audio')<br />
.Nodes.Add('Top1', 4, 'Child1', 'First Child Node', ;<br />
'Desktop')<br />
.Nodes.Add('Top1', 4, 'Child2', 'Second Child Node', ;<br />
'Desktop')<br />
.Nodes.Add('Top2', 4, 'Child3', 'Third Child Node', ;<br />
'Desktop')<br />
.Nodes.Add('Top2', 4, 'Child4', 'Fourth Child Node', ;<br />
'Desktop')<br />
endwith<br />
Summary<br />
<strong>ActiveX</strong> controls are both wonderful and terrible. They’re<br />
wonderful because they can give your applications the<br />
professional, polished look users expect from modern<br />
32-bit applications (the TreeView control that comes with<br />
VFP and the ctListBar control from dbi technologies inc.<br />
are good examples), or they can provide advanced<br />
capabilities that would either be impossible or very<br />
time-consuming to create in VFP code (the DynamiCube<br />
control I discussed in my March 1998 column is an<br />
Downloads<br />
June Subscriber Downloads<br />
• 06DHENSC.ZIP—Source code described in <strong>Doug</strong> <strong>Hennig</strong>’s<br />
article. SFCTRLS.VCX and SFACTIVEX.VCX are class libraries<br />
used to support TREEVIEW.SCX, a sample form that shows<br />
how to add <strong>ActiveX</strong> controls at runtime. SFREGISTRY.VCX<br />
provides access to the Windows Registry.<br />
• 06PADDOC.ZIP—Source code for two items discussed in Rod<br />
Paddock’s article. CODEBLCK.PRG is Randy Pearson’s Code<br />
Block command window replacement. SCRIPTING.SCX is a<br />
sample form that demonstrates how to use the scripting<br />
control in VFP.<br />
Extended Articles<br />
• 06COOL.HTM—Extended Article. How do you keep current<br />
Help is a Mouse-Click Away: Introducing Developer Solutions–Online at www.pinpub.com<br />
When a programming crisis or dilemma comes up, you need<br />
dependable solutions fast. Technical support calls are eating your<br />
time and costing hundreds of dollars—even when you don’t get<br />
solid answers from them. In response to your needs, we’re proud<br />
to announce a new online search mechanism to help you find the<br />
information you need when you need it.<br />
Check out the continually updated Pinnacle Publishing<br />
index of expert-written articles at www.pinpub.com (tell your<br />
colleagues!). You’ll be able to access each monthly issue of<br />
FoxTalk, SQL Server Professional, Smart Access, and Visual Basic<br />
Developer from 1996 through the present. You can view the entire<br />
online Table of Contents of a newsletter or enter a search term to<br />
pinpoint all of the available related articles. The search is free, tips<br />
are free, and articles (including any corresponding Subscriber<br />
Download files) can be downloaded for a nominal fee.<br />
Here’s how it works: Go to www.pinpub.com and click on<br />
Developer Solutions—Online. Enter the search term of your<br />
Portions of the FoxTalk Web site are available only to paid<br />
subscribers of FoxTalk. Subscribers have access to additional<br />
resources to give you an edge in FoxPro development.<br />
example of that). They’re terrible because when “OLE<br />
class not registered” or other OLE errors occur, you can’t<br />
just roll up your sleeves and use the VFP Debugger to<br />
track down the problems. SF<strong>ActiveX</strong> has been a life saver<br />
for me; it handles the main problem I have with <strong>ActiveX</strong><br />
controls. I’m sure you’ll find it as useful as I have. ▲<br />
06DHENSC.ZIP at www.pinpub.com/foxtalk<br />
<strong>Doug</strong> <strong>Hennig</strong> is a partner with Stonefield Systems Group Inc. in Regina,<br />
Saskatchewan, Canada. He is the author of Stonefield’s add-on tools for<br />
FoxPro developers, including Stonefield Database Toolkit and Stonefield<br />
Query. He is also the author of The Visual FoxPro Data Dictionary in<br />
Pinnacle Publishing’s The Pros Talk Visual FoxPro series. <strong>Doug</strong> has spoken<br />
at the 1997 and 1998 Microsoft FoxPro Developers Conferences<br />
(DevCon), as well as user groups and regional conferences all over North<br />
America. He is a Microsoft Most Valuable Professional (MVP).<br />
75156.2326@compuserve.com, dhennig@stonefield.com.<br />
with everything that’s happening to the wide array of tools<br />
you’re using? Woody’s Office Watch is a free weekly e-mail<br />
that covers everything that’s happening with Microsoft<br />
Office. Also covered: Directory Toolkit, a shareware tool that<br />
pulls together a number of features that you’d like to see in<br />
Windows Explorer.<br />
• 06COOL.ZIP—Contains SETUPDT.EXE, the installation file for<br />
Directory Toolkit, a shareware utility described in this<br />
month’s Cool Tool column.<br />
• 06PETER.HTM—Extended Article. This month, John Petersen<br />
addresses the topic of where to place your Automation<br />
code: in the VFP application, in the Office application, or, in<br />
the case of Tahoe, in a VFP COM object.<br />
choice to access any of the material we’ve published on<br />
this topic. Alternatively, you can click on “Contents” on the upperleft<br />
side of the screen to list any of the published material by<br />
issue date.<br />
Possible uses:<br />
• Use the search function to find an article you can’t locate<br />
in your back issues.<br />
• Find all of our published articles on a given topic.<br />
• Use the pay-per-view option to access articles published<br />
before your subscription started by following the online<br />
sign-on instructions. (You’ll be able to access the download<br />
file as well.)<br />
• Browse the “Contents” portion for summaries of articles and<br />
free tips that have been published in FoxTalk and our other<br />
publications.<br />
Coming soon: Online subscription options!<br />
User ser name<br />
Passw assw asswor or ord<br />
kerosene<br />
lantern<br />
24 FoxTalk June 1998<br />
http://www.pinpub.com