Sie sind auf Seite 1von 72

Features

6 VFP 8: A Great Tool For Data-Centric Solutions


Eric Rudder

8 VFP 8: Visual FoxPros Biggest


Update Since Version 3.0

20 The CursorAdapter Revolutionizes Data Access


The CursorAdapter class is one of the most impressive accomplishments of the VFP 8 development team. It will change the
way many developers relate to their various data sources.
Chuck has wrestled with this class and reveals its inner secrets.
Chuck Urwiler

Ken Levy

10 VFP 8 Feature Highlights


There are so many new features in VFP 8 that we can't cover
them all. Claudio does his best to hit the high points in this
round-up of several exciting new capabilities that VFP 8 brings
to the table.
Claudio Lassala

14 Collections are Cool!


Visual FoxPro 8.0 includes a new Collection base class. In this
article, Doug describes some uses for collections and why they
are superior to arrays.
Doug Hennig

16 Event Binding in VFP 8


Event Handling in VFP 8 opens a new world to developers
who need to hook various events to custom code. With the
new BINDEVENT() function, Microsoft has handed over a
fabulous new capability to those who dare to use it.
Markus Egger

38 Structured Error Handling in VFP 8


Visual FoxPro 8.0 introduces a new error handling mechanism
known as Structured Error Handling. This mechanism uses
Try/Catch blocks to wrap sections of source code and attach
the appropriate error handler. Markus takes you on an adventure in error-land.
Markus Egger

48 Member Classes Bring Flexibility


By changing the way you can define and work with member
classes, Microsoft has given VFP developers tremendous flexibility in working with pageframes and grids. Garrett gives you
a few clues to get started.
Garrett Fitzgerald

50 The VFP 8 XMLAdapter Class


The new XMLAdapter class in VFP 8 greatly enhances your ability to work with XML data sources, including hierarchical XML
documents, such as .NET DataSets. Cathi explains how this and
related new classes open new possibilities for your applications.
Cathi Gero

58 My Favorite Feature
We asked for input from several early adopters of VFP 8 about
their favorite new features. You'll sense their enthusiasm as
you read their suggestions for why you should step up to the
latest and greatest version.
The Community

64 VFP 8 Tips and Tricks


Here you'll find an assortment of great ideas for working with
VFP 8. Early adopters spill the beans about some of the most
interesting aspects of the new features.
The Community

66 Creating a StatusBar Control with VFP 8


Visual FoxPro 8 offers full support for themes and the XP style
look. Unfortunately, the Windows Common Control OCX that
ships with VFP 8 doesnt support this same look. This article describes how to build a status bar control that looks and behaves
like an XP style status bar, using several new features of VFP 8.
Rick Strahl

Departments
65 Code Compilers
49 Advertisers Index
4 code-focus

VFP 8: A Great Tool


For Data-Centric Solutions
i, Im Eric Rudder, Microsoft Senior Vice
President for the Developer & Platform
Evangelism Division. Id like a take a few
minutes and talk to you about Visual FoxPro
8.0 and our efforts involving VFP. Microsoft
has been evolving FoxPro for the past 10 years
and I am very enthusiastic in telling you that
we are committed to continuing to evolve
Visual FoxPro as a great tool for building datacentric solutions.

Eric Rudder

I had the privilege of being the architect of


Visual FoxPro 3.0 and many of the great
features we brought to Fox, such as the visual
class designer, the object model, and the
database container. Ive been involved in
reviewing every version of VFP since version
2.5, and I am thrilled with all the great new
features in VFP 8.
Many of the new features were taken directly
from customer feedback. Beyond building
stand-alone applications, VFP 8.0 greatly
extends interoperability with SQL Server
2000 and Visual Studio .NET. These new
capabilities include VFP cursor data integration with XML and ADO.NET.
VFP 8.0 fully supports creating and
consuming .NET compatible XML Web
services, allowing VFP to publish or receive
any type of data from any type of system or
device whether it be a mainframe, PCs,
PDAs, Tablet PCs, or even wireless phones.
This is especially true when using VFP with
ASP.NET Web forms. ASP.NET is a great
complimentary tool for Web development.
The Fox OLE DB provider and XML Web
services support in VFP 8.0 are great additions for .NET integration.
We love to receive input on how to make
better developer tools to meet your needs.
Here at Microsoft, community relations are
increasingly important throughout the
company. Bill Gates, Steve Ballmer, and I
are very supportive of community efforts, and
we view the FoxPro community as a great
example of communities and Microsoft
working together.
I encourage you to find out more about
Visual FoxPro and .NET technologies on
MSDN and GotDotNet.com. I look forward
to hearing success stories of applications

6 code-focus

created using Visual FoxPro 8.0 combined


with our other great products and technologies. Thank you.

Eric Rudder
Senior Vice President, Developer
and Platform Evangelism
Microsoft

Eric Rudder currently serves as senior vice president of Developer and Platform Evangelism. He
is responsible for leading the companys
outreach to the developer community. Through
his leadership of this division, Rudder and his
team focus on coordinating the overall programming model for the client and server, creating
the best tools for the .NET platform and fostering
synergies between Windows and the .NET Enterprise Server offerings. In addition, the division
will be chartered with evangelizing the extended
Microsoft platform through a variety of content
offerings including providing effective webbased training for developers and IT managers.
Prior to leading the Developer and Platform
Evangelism division, Rudder was vice president
of Technical Strategy working directly with
Chairman and Chief Software Architect Bill Gates
on Microsofts technical planning processes. He
also has worked in several other areas at
Microsoft, including networking, operating
systems and developer tools, where he previously served as general manager for Visual
Studio.

A video this message from Eric Rudder


on Visual FoxPro 8.0 can be viewed and
downloaded from
http://gotdotnet.com/team/vfp.

code-focus

VFP 8: Visual FoxPros Biggest


Update Since Version 3.0
he Visual FoxPro Team at Microsoft is
very excited to announce that Visual
FoxPro 8.0 was completed and released to
manufacturing (RTM) on January 31st, 2003.
The Visual FoxPro team was hoping to release
Visual FoxPro 8.0 in early January, but we
needed the extra time to ensure we released
the most stable version of Visual FoxPro yet,
even more solid than Visual FoxPro 7.0 with
Service Pack 1. Visual FoxPro 8.0 is the most
feature rich release of Visual FoxPro since
version 3.0. Id like to include some details
about Visual FoxPro 8.0, related announcements, and upcoming activities around the
release.

Ken Levy

Visual FoxPro 8.0 is fully compatible with


previous versions of Visual FoxPro and
includes everything needed to write, build,
and test great data-centric applications.
Product highlights in Visual FoxPro 8.0
include structured error handling, Cursor
Adapter class for universal data access, new
auto-increment support for data tables,
Windows XP Themes support, new base
classes and controls, new GDI+ image
support, new event binding for objects, full
hierarchical support, new XML Web services
features, updated OLE DB provider,
improved development productivity, and new
and improved compatibility with Visual
Studio .NET and SQL Server 2000.

TRY...CATCH...FINALLY structure, escalate


exceptions using THROW, and manage using
Exception objects.
Universal data access - Use the Cursor
Adapter class for unified data access to
native Visual FoxPro tables, XML,
Microsoft SQL Server 2000, and ODBC or
OLE DB data sources.
View Designer - Build SQL SELECT statement queries using two-way editing
between the Query/View designer and
source code output.
Auto-increment support - Create autoincrement counter fields for primary key
fields in Visual FoxPro data tables.
Windows XP Themes support - Apply
Microsoft Windows XP Themes to Visual
FoxPro applications, including controls at
the form and container level.
Advanced controls - Build smart client
desktop forms using rich features such as
orienting tabs on page frames, locking and
automatically resizing columns, highlighting grid rows, changing background
color and positioning images on command
buttons.
GDI+ image support - Display and rotate
images on forms with support for file
formats such as .bmp, .jpeg, .gif, animated
.gif, .wmf, .emf, .tiff, and more.

Visual FoxPro 8.0


Product Highlights:
Structured error handling - Control and
encapsulate error handling in code using

Event binding - Bind native and custom

Visual FoxPro 8.0 System Requirements:


Hardware
Processor
RAM
Hard disk space required to install
Hard disk space required
Video

PC with a Pentium-class processor or higher


64 MB; 128 MB or higher recommended
300 MB
165 MB
800x600, 256 colors

Design-time

Windows 2000 with Service Pack 2,


Windows XP Professional or later
Windows 98/Me, Windows 2000,
Windows XP or later

Software

Run-time

8 code-focus

events and methods to other Visual FoxPro


objects.
XML data import and export - Import and
export hierarchical XML using support for
XML DiffGrams between Visual FoxPro
data cursors and ADO.NET DataSets.
XML Web services - Publish and consume
SOAP-based XML Web services compatible with .NET technology over HTTP
using the Microsoft SOAP Toolkit 3.0
(included).
OLE DB Provider - Provide access to Visual
FoxPro data from other clients such as
Microsoft Visual Studio .NET and Office XP.
Microsoft SQL Server 2000 Desktop
Engine (MSDE) - Build desktop and shared
solutions compatible with Microsoft SQL
Server 2000 and migrate them to SQL
Server 2000 directly without modifying
code.
Deployment tools - Create setup programs
for your applications using an exclusively
developed version of InstallShield
Express for Visual FoxPro.
Powerful base classes - Subclass member
classes such as pages in page frames and
headers and columns in grids. Subclass the
DataEnvironment class and use the
DataEnvironment and CursorAdapter
builders. You can manage application
objects using the new Collection class.
Development productivity - Use the Task
Pane to manage development tasks and
access Visual FoxPro Help, XML Web
services, and community news. The new
Toolbox provides easy access to frequently
used items such as classes, controls, and
code snippets. You can search for and
replace symbol and text references
throughout projects and files using the
Code References tool.
The release date for Visual FoxPro 8.0
(January 31, 2003) just happened to be same
date as the last day before expiration of the
public Visual FoxPro 8.0 beta, by coincidence.
Developers using the current public beta of
Visual FoxPro 8.0 (build 2021) can change the

system date on their computer to use it until


obtaining the released version via MSDN
Subscriptions or stand-alone product box.

download of Eric Rudder (Sr. VP Developer


& Platform Evangelism Division, Microsoft)
discussing Visual FoxPro 8.0.

Visual FoxPro 8.0 is included in all levels of


MSDN Subscriptions (Universal, Enterprise,
and Professional). Also included in those
levels of MSDN Subscriptions are Visual
FoxPro 8.0, Visual FoxPro 7.0, Visual Studio
6.0, Visual Studio .NET, upcoming Visual
Studio .NET 2003, development versions of
Windows, and more. Refer to http://msdn.
microsoft.com/subscriptions/prodinfo/levels.asp
for product information of MSDN Subscription levels. MSDN Subscription members will
be first to obtain the released version of
Visual FoxPro 8.0 (within a few business days
of RTM, first week of February). Pricing
information for MSDN Subscriptions can be
found at http://msdn.microsoft.com/subscriptions/prodinfo/pricing.asp (upgrade pricing of
MSDN Subscriptions available for licensed
users of Visual FoxPro 5.0 or later).

The Visual FoxPro team has already started


working on the next version of Visual FoxPro
(code named Europa) and we are now taking
feedback and suggestions for features via the
Wish List on the UniversalThread (http://
www.universalthread.com) as well as other
Visual FoxPro community sites online. The
main calls to action for Visual FoxPro
community members are to upgrade to Visual
FoxPro 8.0, convince others to upgrade, and
educate others about the great new features
in this new version.

Visual FoxPro 8.0 will be available separate


from MSDN subscriptions via stand-alone
full product or upgrade purchases 6 to 8
weeks from the RTM date (around mid
March). U.S retail prices for the stand-alone
versions of Visual FoxPro 8.0 include: full
product $649, upgrade $349, and academic
$75. For a limited time, a $50 rebate will be
offered with Visual FoxPro 8.0 (for upgrade
version only, in U.S. and Canada only) for
owners of Visual FoxPro 7.0 (proof of
purchase required with mail-in rebate
coupon).
There is now a Visual FoxPro 8.0 Case Study
Template available for download (Word
document format) at the Microsoft
GotDotNet.com developer community Web
site on the Visual FoxPro with Visual Studio
.NET page at http://gotdotnet.com/team/vfp. This
case study template is designed to be used for
submissions to Microsoft for possible
microsoft.com product showcasing for applications using or in the process of upgrading
to Visual FoxPro 8.0. This case study
template is also intended to be used independently of Microsoft by Visual FoxPro developers to create case studies to be posted on
Visual FoxPro community Web sites. Also
available on the GotDotNet site on the
Visual FoxPro page is a 2.5 minute video

Additional information about Visual FoxPro


8.0 will be found at http://msdn.microsoft.
com/vfoxpro in early March. Included in the
new MSDN Web site content to be added to
the Visual FoxPro Web site on MSDN next
month will be a detailed Visual FoxPro Evaluation Guide as well as new technical
whitepapers. Also to be posted will be new
Visual FoxPro 8.0 code samples and utilities
for free download (which will displayed in
the new Visual FoxPro 8.0 TaskPane when
refreshed). The MSDN Visual FoxPro
product download section will also soon
include a download of the Visual FoxPro
OLE DB Provider, same as the version
included in Visual FoxPro 8.0 but a separate
free independent download for anyone to
use. Stay tuned to further announcements on
the Visual FoxPro community news site
http://foxcentral.net.

Ken Levy
Visual FoxPro Product Manager
Microsoft

Ken Levy is the Microsoft Visual FoxPro Product


Manager. He has developed many high-profile
applications and tools in all versions of FoxPro
since 1986. He is the author of many components of VFP including the Class Browser and
Component Gallery. Ken is also the author of
GenScrnX. Ken has contributed as a technical
editor and writer to many software magazines
and is a frequent speaker at industry conferences.

code-focus

VFP 8 Feature Highlights


Visual FoxPro 8 includes numerous new features that are a direct
response to the requests of VFP developers. Just reading through the
Whats New section of the documentation will take you quite a while due to the
large quantity of additional or changed features and commands. Lets take a brief
look at just a few of the exciting new capabilities that you can put to use immediately.

Claudio Lassala
claudio@eps-software.com

Claudio Lassala is a Senior


Developer at EPS Software
Corporation and a part time
Universal Thread Consultant.
He had presented several
lectures at Microsoft Brazil and
VFP conferences as well. He is
a two-years recipient for the
Microsoft MVP Award and also
a columnist in MSDN Brazil. He
is the author of several training
videos for Visual FoxPro that
can be found in the Universal
Thread, inclusively a 40-minute
video called Whats new in
VFP 8, that shows off several
enhancements of this new
version.

ot only does the documentation of new features


in VFP 8 take up a large number of pages, but
there are more than 100 new keywords added to
the language. These represent additional Commands,
Functions, Class names, Properties, and Methods.

You will have to explore VFP 8 on


your own to discover many of the
great new additions, but I will give
you a quick tour of some of the
new feature highlights that have
caught my attention.

Insert-SQL changes

meant to abstract relations between tables, while a


Line class is meant to be a visual line in a UI control. The Empty class enters the picture.

The Empty class does not have any intrinsic properties, methods or events (yes, thats
why it is called empty). Not havFast Facts
ing any members, this class gets
instantiated and destroyed very
There are so many new features in
quickly. Of course, this class is
VFP 8 that we cant cover them all.
useless as-is, because if it doesnt
have an AddProperty method like
Claudio does his best to hit the
all the baseclasses in VFP, how
high points in this round-up of
can we make any use of it?
several exciting new capabilities

that VFP 8 brings to the table.

The Insert-SQL command has


gained two great new capabilities. It can now accept data that comes from an object or from a Select-SQL statement.

For example, if you have an object (very likely a business object) that among its PEMs has properties that
match fields in a cursor, you can insert data into the
cursor right from the object. The syntax is very trivial:
Insert into curProducts from name oProduct

Note that oProduct is the name of the business object that has the data. This feature lets you avoid the
need to append a blank record to the cursor and
then fill the fields with the gather command, as in:

The answer is two more new features: the AddProperty and RemoveProperty functions. Despite the fact that these
functions can work with any object, their biggest
role is to support the Empty class. The use of these
functions and the Empty class is very trivial:
oCustomer = CreateObject("Empty")
AddProperty(oMonths, "LastName")
AddProperty(oMonths, "FirstName")
oCustomer.LastName = "Lassala"
? oCustomer.LastName

For removing a property, it goes like this:


Append Blank in curProducts
Gather from name oProduct

Here goes the other new feature: instead of having


to get a resultset from a query and scan through it
populating a cursor, now you can do all at once:
Insert into curProducts ;
(ProductID, ProductName, UnitPrice);
Select ProductID, ProductName, UnitPrice ;
from Products Where UnitInStock > 20

An Empty object?
For a long time, developers have been looking for
a lightweight class, mainly when they have to add
and remove properties to an object on the fly.
Some people have been using the Session or the
Custom classes, but those are not really intended
to be lightweight classes. Others use classes like
Relation or Line, but that is kind of weird from an
OOP point of view, because a Relation class is

10 code-focus

RemoveProperty(oMonths, "LastName")

Another use for the Empty class comes with using


the Scatter command. The Scatter Name oObject
command creates an object that has one property
for each field in the cursor or table in the current
work area, storing the values of the fields into the
properties in the brand new object.
But, now we can create an Empty object beforehand, which has some additional properties besides
the ones that match with the fields in the cursor,
and then use the new ADDITIVE clause to the
Scatter command, which will cause the use of an
existing object, instead of a brand new one.
For example, if we still have in memory the oCustomer object for the previous sample, we can scatter
fields from the customer table to it like this:
Use Customer
Scatter Name oCustomer Additive

An Empty class is a sealed class, which means that


it cannot be sub-classed.

Lets now see a sample that plays with the new features
both in the Insert-SQL and the Scatter commands.

Putting some new features together

Often, we need to keep track of the size and location of forms as they were the last time they were
destroyed (the user runs a form, changes its size and
location, then closes the form). The next time the
user runs the form, he expects that it will be shown
the same way.

A practical use for an Empty object is getting rid of


some of those public variables (or private variables in
the main program) that some developers have spread
through their applications, especially when theyre
not willing to move to an entirely OOP approach.
For example, usually some developers have a few
public variables that store environment information,
like the name of the Machine, the OS version, the
users name, and so on. This is a simple sample of
how they could get rid of a few of those variables by
using an Empty object:
Private poApp
*-- We create an "empty" object.
poApp = CreateObject("Empty")

Doing that now is a piece of cake. Basically, we


need a simple table that has numeric fields for
keeping the size of a form and its position (Top,
Left, Width, and Height). In the Destroy event
method of the form, we put this line of code:
Insert into Settings from name Thisform

Of course, we need to apply some logic to check


whether we already have a record in the table
keeping track of these values. If thats the case, we
can just update the record with this line of code:
Gather name Thisform

*-- We add a few properties to the object.


AddProperty(poApp, "MachineName",;
Substr(Sys(0),1,At("#",Sys(0))-1))
AddProperty(poApp, "OSVersion", Os(1))
AddProperty(poApp, "UserName",;
Substr(Sys(0),At("#",Sys(0))+1))

You can see that we create just a single property


that will store a reference to an Empty object, and
then we use the new AddProperty function to add a
few properties to the object (MachineName, OSVersion and UserName). We are using the
AddProperty function to also initialize the values of
the properties, but we could assign values to the
properties in an Object.Property approach, like:
poApp.OSVersion = Os(1)

Then, you can use this object anywhere:


MessageBox("User Name: "+poApp.UserName+Chr(13)+;
"Machine Name: "+poApp.MachineName+Chr(13)+;
"OS Version: "+poApp.OSVersion)

Both lines of code will take the Thisform reference


to the form object, and then grab the value of its
Top, Left, Width and Height properties to replace in
the table.
Now, in the Init event method, we grab the values
stored in the table and then update our object properties with this line of code:
Scatter name Thisform additive

DataEnvironment Class and Builder


Lets picture a common situation: you have a form
with a beautiful (read complex) DataEnvironment
defined, with lots of tables, relations, ordering, and
so forth. Then you face a new need where youll
have another form that requires the same settings of
the first one. Until VFP 7, you had two choices:
recreate everything again from scratch manually or
create the DataEnvironment programmatically.
That was because we werent able to define and
subclass the DataEnvironment class. Now we are!
Now, with VFP 8, we can just go to the form that
has the DataEnvironment defined, and then select
Save As Class from the File menu. In the dialog box
youll find a new DataEnvironment option. Then,
its just a matter of typing the name of the class,
where it should be stored and a description for it
(see Figure 1)
The Form baseclass was changed for supporting the
DataEnvironment class. Two new properties were
added: DEClass and DEClassLibrary (where you
type the name of the Class and the Class Library file
where the class is located, respectively).

Figure 1: Using Save as Class to save a DataEnvironment


Class.

From the VFP Team:

Randy Brown
Lead Program Manager,
Visual FoxPro
Microsoft
After we shipped Visual FoxPro
7.0, the VFP team sat down and
spent quite a few weeks
reviewing over 1000 enhancement requests (ERs) that we
had collected in our internal
Foxwish database. This database contains ERs from a
variety of sources included past
betas, Universal Thread,
internal, old aliases, user
groups, conferences, etc. We
had certain goals, objectives
and criteria in mind when we
evaluated the ERs.
In the end, because the FoxPro
community has become so
diverse in how folks develop
apps, we felt that it was critical
to maintain a strong mix of
features so that VFP 8.0 would
contain something for
everyone. We wanted VFP 8.0
to be a no brainier upgrade
and we think we have come
pretty close to achieving this.
And if we add some new
customers to the FoxPro
community, even better. Note:
we estimate that over 80% of
the features in VFP 8.0 are from
ERs submitted by you guys in
the community. You could say
that this is certainly the
peoples FoxPro.

You can define DataEnvironment classes both in


the Visual Class Designer (stored in VCX files), or
programmatically (stored in PRG files). Of course,
the Form is able to work with both equally well. A

code-focus

11

sample of a PRG-based DataEnvironment class can be found in


Listing 1.
Sure enough, you are not
limited to just using DataEnvironment classes within Forms.
You could use the class defined
in Listing 1 anywhere with
something like this:
Figure 2: The result of instantiating a DataEnvironment
apart from a form.

GDI+ Support
VFP 8 has support for GDI+.
That means that we can use the
most traditional image formats
in those properties that store
reference to an image file (like
the Picture property). Inclusively, we can use an Animated
GIF, getting rid of specialized
OCX or the Web Browser
control just to show that kind of
image.
If the image is inside an Image
control, we can also do things
like Rotate and Flip the image
(through the use of the RotateFlip property of the Image
control).

Windows XP Themes
Support
VFP 8 supports Windows XP
Themes by default. In other
words, our VFP applications
can have a very modern look,
following the skin set to the
Operating System, without even
a single line of code. But if in
any case you want to control
the Themes capability, you can
set that with a good granularity,
in an application level, form
level or control level.

oDE = CreateObject("CustomersOrders")
oDE.OpenTables()

You can run those lines of code and then go to the


Data Session window, and see the results (Figure 2).

Table changes
One of the long-waited features that now is present
in VFP 8 is the AutoIncrementing field for tables
(usually used for keeping primary keys in a table).
VFP has a new field type called Integer (AutoInc)
that addresses this issue.
There is also a minor enhancement for the indexes
of a table: a collating sequence can be defined individually for every single index tag.
Another very neat new feature is that an expression
can be defined as the Caption of a field in the Table
Designer. This expression is evaluated every time a
Browse window is opened. However, theres something much cooler about this. In earlier versions, we
could drag a field from a DataEnvironment and drop
it onto a form, and that would create a label with its
Caption property set to the caption of the field.
In VFP 8, the caption of the label will be evaluated
at run-time, which gives us the flexibility for datadriven labels in our forms. For example, I could set
the Caption of the CompanyName field to the
following expression:
=Localize("CompanyName")

Localize is just a Stored Procedure I have that


will return the localized version (in this sample,
the Portuguese version) of Company Name,
according to the Regional Settings running on this
computer (the implemenation doesnt matter
here). In Figure 3, you see the label thats evaluated every time the form runs.
The Field() function supports this new feature, by
means of a parameter that dictates whether the
return of the function will be the physical name of
the field or the evaluated expression of the Caption.
Looking again at Figure 3, you see a ToolTip close
to the Textbox. In the Init method of that Textbox I
have this line of code:
This.ToolTipText = Field("CompanyName",
"Customers", 1)

The value 1 in the third parameter says that the


return of the function will be its evaluated expression on the Caption property.

12 code-focus

Grid Enhancements
It is common knowledge that VFPs Grid is one of
the most powerful and most used controls. It is easy
to use and provides great functionality. However,
VFP developers have always dreamed of some
enhancements to the Grid to make it even better. In
VFP 8, several dreams have come true. Figure 4
shows some of the features that I will explain below.

Acting like MS-Excel


Everyone who uses MS-Excel is used to the way it
works in regard to resizing columns (auto-fitting the
columns to match their largest content), which can
be done by double clicking in the line that divides
one column from another, causing the column at
the left to be resized. Alternatively, we can double
click the square at the left-top of the spreadsheet
and that will cause all the columns to be resized.

Figure 3: The label on the form is dynamically evaluated at


runtime.

VFPs Grid now behaves just the same. You can see
in Figure 4, between the Contact Name and
Contact Title columheaders, the mousepointer
indicating the divider line. After double clicking, the
Contact Name column is resized.
Not just that: we can also Auto-fit columns in the
grid programmatically:
*-- Resize second column of the grid
Thisform.grdCustomers.AutoFit(2)

Listing 1: A DataEnvironment class definition


DEFINE CLASS CustomersOrders AS dataenvironment
Name = "dteCustomersOrders"
ADD OBJECT crsCustomers AS cursor WITH ;
Alias = "Customers", ;
Database = "northwind.dbc", ;
CursorSource = "Customers"
ADD OBJECT crsOrders AS cursor WITH ;
Alias = "Orders", ;
Database = "northwind.dbc", ;
CursorSource = "Orders"
ADD OBJECT relCustomersOrders AS relation WITH ;
ParentAlias = "Customers", ;
RelationalExpr = "customerID", ;
ChildAlias = "Orders", ;
ChildOrder = "customerID"
ENDDEFINE

*-- Resize fourth column of the grid


Thisform.grdCustomers.AutoFit(4)
*-- Resize all of the columns at once
Thisform.grdCustomers.AutoFit(0)

This feature (and some other new features) also


works with the BROWSE command. For example:
BROWSE Name oMyBrowse NoWait
oMyBrowse.AutoFit(0)

Another Excel-like feature is the ability to freeze


columns. In Figure 4 we see a thicker line dividing
the Contact Title and the Country columns.
That line indicates that all the columns at the left
side of it are frozen. When scrolling to the right side
of the grid, we will never lose sight of them. Here is
the the code required to freeze the first four
columns of the sample grid:
Thisform.grdCostumers.LockColumns(4)

Grid Highlighting
VFP developers have always had trouble working with
highlighted rows in a grid. End-users want the ability to
select a row in a grid and keep track visually of which
row is selected, even when the focus is in another place
(see the third line of the grid in Figure 4). We usually
end up writing a whole bunch of code to make that
work. Now, this line of code is all we need:
Thisform.grdCustomer.HighlightStyle = 2

Also, we have two properties to configure the backcolor and forecolor of the highlighted row: HighlightBackColor and HighlightForeColor.

A Grid as a Listbox
Previously, if we clicked in a row of the grid, the
row would be highlighted and the cursor would be
blinking inside the cell (usually in a textbox). Sometimes we do not want the user to have access to the
cell. Rather, we just want the user selecting a
record, imitating a Listbox (remember that a
Listbox does not has the Grids capabilities to bind
to data, among other things).
To address that, there is another welcomed new
property to the Grid: AllowCellSelection.
*-- Do not allow the user to select cells.
Thisform.grdCustomers.AllowCellSelection = .F.

Hiding and Showing Columns


Another feature that used to require several lines of
code is hiding and showing columns in a grid. There
was not a single straight-forward way to do that.
Now, all it takes is:
*-- Hide second Column
Thisform.grdCustomers.Columns(2).Visible = .F.

Figure 4: An example of many grid enhancements.


*-- Show fourth Column
Thisform.grdCustomers.Columns(4).Visible = .T.

This makes it very easy to show or hide columns


depending on the user rights to see certain data, for
example.

Centering Checkboxes
Often, grids are used to allow the user to select one
or multiple records (look at the Select column in
Figure 4). In those situations, we insert a Checkbox
in a column of the Grid, and guess what? We
cannot make the Checkbox center itself within the
column. The workaround was to insert the
Checkbox centered in a Container, and then insert
the container into the column.
Once again, we can reduce that now to a single line
of code by using the Centered property of the
Checkbox (I did split it here into three lines to fit
the layout of the magazine):
*-- Centralize the Checkbox the eighth column.
With Thisform.grdCostumers
.Columns(8).chkSelect.Centered = .T.
EndWith

Images on the Headers


Last but not least, we see that two columns have
images on their headers (columns Company
Name and Phone). To add an image to a header
we set the Picture property:
With Thisoform.grdCustomer
.Columns(2).Header1.Picture = "Customer.ico"
.Columns(6).Header1.Picture = "Phone.ico"
EndWith

You may have noticed that I am using an ICO file


(which is an icon image) instead of a BMP or other
graphics image. See the sidebar GDI+ Support for
more information about this.

Summary
Although I could go on and on with dozens more
examples of new features, space is limited in this
magazine. So, get your hands on a copy of VFP 8
and dig right in. Youll soon be as excited as I am.

Taskbar Notification
Area (the System
tray)
The Solutions Samples bring a
Systray Class. This class lets
the developer add items (icons)
to the System Tray in a very
easy way, wrapping up the
complex calls to the Windows
API. Its possible to add a
regular VFP menu to the item in
the Systray, or even use those
balloon tips that we see in
Windows XP (if the application
is running in that OS).

Docking Windows
support
VFP 7 brought the ability to
manually dock windows in its
IDE. Now VFP 8 brings the
ability to do that programmatically in the IDE, with the new
ADockState function and the
new DOCK WINDOW
command.
The ADockState function
creates an array with the status
for the dockable IDE windows
and toolbars. The DOCK
WINDOW is self explanatory,
and you can find its syntax in
the documentation.

Support for more


classes in the Visual
Class Designer
VFP 8s Visual Class Designer
also brings added support for
the following classes: DataEnvironment, Cursor, Relation,
Page, Collection,
CursorAdapter, XMLAdapter,
XMLField, and XMLTable. This
means that all of these classes
can be defined visually and
stored in VCX files.

Claudio Lassala

code-focus

13

Collections are Cool!

Doug Hennig
dhennig@stonefield.com

Doug Hennig is a partner with


Stonefield Systems Group Inc.
He is the author of the awardwinning Stonefield Database
Toolkit (SDT), the awardwinning Stonefield Query, and
the CursorAdapter and DataEnvironment builders that ship
with Microsoft Visual FoxPro 8.
Doug is co-author of Whats
New in Visual FoxPro 8.0, The
Hackers Guide to Visual
FoxPro 7.0, and Whats New
in Visual FoxPro 7.0. He was
the technical editor of The
Hackers Guide to Visual
FoxPro 6.0 and The Fundamentals. All of these books are
from Hentzenwerke Publishing,
(www.hentzenwerke.com).

Doug writes the monthly


Reusable Tools column in
FoxTalk. He has spoken at
every Microsoft FoxPro Developers Conference (DevCon)
since 1997 and at user groups
and developer conferences all
over North America. He is a
Microsoft Most Valuable
Professional (MVP) and Certified Professional (MCP).
www.stonefield.com
www.stonefieldquery.com

Collections are a common way to store multiple instances of things.


For example, a TreeView control has a Nodes collection and Microsoft Word has a
Documents collection. Until recently, Visual FoxPro developers wanting to use
collections often created their own classes that were nothing more than fancy
wrappers for arrays. However, in addition to being a lot of code to write, homebuilt collections dont support the FOR EACH syntax, which is especially awkward
when theyre exposed in COM servers. Visual FoxPro 8.0 solves this problem by
providing a true Collection base class.
he Collection base class has only a few properties, events, and methods. The Add method adds
an item to the collection, the Remove method
removes an item, and the Item method returns an
item. The Count property indicates how many items
are in the collection. An item can be a scalar value
such as text or a number, but most commonly is an
object. In addition to the item itself, the collection
can store a key for the item, such as a name. An item
can be located in the collection by:

Position: Collection.Item(2) returns the second


item in the collection
Key: Collection.Item(Doug) returns the item that
has Doug as its key
Since Item is the default
method, you can omit it if
desired; Collection.Item(2) and
Collection(2) do the same
thing.

lnForms = alen(This.aForms, 1) 1
lnCols = alen(This.aForms, 2)
if lnForms = 0
This.aForms = .NULL.
else
* lnForm is the row number of the closed form
adel(This.aForms, lnForm)
dimension This.aForms[lnForms, lnCols]
endif

Fast Facts
Visual FoxPro 8.0 includes a
new Collection base class.
In this article, Doug describes
some uses for collections and why
they are superior to arrays.

Collections can be simple


replacements for arrays. Each
item in a collection is similar to
a row in an array. However,
because theyre objects, collections have many more capabilities than arrays. This
article will look at three specific uses for collections.

Use Collections Instead of Arrays


Some objects need to store a collection of things.
For example, a forms manager needs information
about every open form in an application. In addition to an object reference to the form, it may also
keep information about which toolbar the form uses
(so you dont have multiple instances of the same
toolbar), whether the form was added to the
Window menu or not, the instance number of the
form (in case the same form can be opened more
than once), and so on. Until VFP 8, this information
was often kept in an array, with one row per form
and one column for each type of information.
However, as the number of columns increases, it
becomes more and more difficult to keep track of
whats in the array and where. Was it the fourth
column that stored the instance number or the

14 code-focus

seventh? Also, because arrays in VFP cant have 0


rows, you have to be careful removing items from
the array when a form is closed:

This complexity disappears


when you use a collection.
Instead of a row in an array, a
form is represented by an
object in a collection. The
object contains a reference to
the form and the other information required. Which code
would you rather write (or
read, for that matter) to find the
instance number of a form?

* Array-based code
lnPos = ascan(This.aForms, CustomerForm)
lnRow = asubscript(This.aForms, lnPos, 1)
lnInstance = This.aForms[lnRow, 4]
* Collection-based code
lnInstance = ;
This.oForms(CustomerForm).nInstance

Removing an object from the collection is easy,


because theres no need to worry about array
dimensions. Simply call the Remove method of the
collection.

Pass Collections as Parameters


Suppose you want to call a function that fills an
array and the array is a member of an object. Other
than a kidney stone, there isnt anything harder to
pass than a member array. Since arrays must be

passed by reference using the @ operator, and you


cant use @ with a member array, you have to pass
a local array and then ACOPY() the local array into
the member array. However, to avoid an error, you
must DIMENSION the member array properly first.
I have a lot of code similar to this in various applications:
dimension laItems[1]
SomeFunction(@laItems)
lnRows = alen(laItems, 1)
lnCols = alen(laItems, 2)
dimension This.aItems(lnRows, lnCols)
acopy(laItems, This.aItems)

Using a member collection rather than a member


array (and assuming SomeFunction can work with
a collection), this becomes as simple as:
This.oItems = createobject(Collection)
SomeFunction(This.oItems)

Use Collections of Collections


The items in a collection can be anything, including
other collections. In addition to acting like multidimensional arrays, collections of collections allow
you to address objects at any level of detail using
simple syntax.

If Tables is a collection of Table objects, and a Table


object has a Fields collection of Field objects, and a
Field object has a DataType property, this is easily
done.

From the VFP Team:

Listing 1 shows an example of this. The Init method


of the Tables class populates the collections of tables
and fields by reading meta data from a table called
CoreMeta.dbf. This table has columns of information
about tables and fields, including cRecType (T for
a table and F for a field), cObjectNam (the name
of the table or field), and cType (the data type of a
field).
To determine the number of fields in the Customer
table, use the following:

Calvin Hsia

Tables(customer).Fields.Count

You can get the descriptive name for the


Orders.Order_Date field with this:
Tables(orders).Fields(order_date).Caption

Summary
The new VFP 8 Collection base class makes it easy
to create and work with collections of items. While
arrays still have their place, I predict collections will
replace them in the majority of uses in applications
as VFP developers become more familiar with them.

Doug Hennig
Suppose you want to work with meta data.
Wouldnt it be nice to retrieve the data type for a
field using code like the following?

Lead Developer,
Visual FoxPro Team
Microsoft
Collections have been desired
and emulated in previous
versions of VFP by developers
whove seen them in other
languages. Now in VFP 8.0, the
new Collection base class
allows you to write better code
with improved performance.
Not only is the Puzzle back in
VFP 8.0, but the new TaskPane
contains a Minesweeper game
written in FoxPro which I
wrote and uses the new
Collection class. Cool!

Tables(Products).Fields(ProductID).DataType

Listing 1: Creating meta data as collections of collections


define class Tables as Collection
procedure Init
local lcTable, loTable, lcField, loField
use CoreMeta
scan
do case
* If this is a table, add it to the collection.
case CoreMeta.cRecType = T
lcTable = trim(CoreMeta.cObjectNam)
loTable = createobject(Table)
This.Add(loTable, lcTable)

.Decimals = CoreMeta.nDecimals
.Binary
= CoreMeta.lBinary
.AllowNulls = CoreMeta.lNull
.Caption
= trim(CoreMeta.cCaption)
endwith
This.Item(lcTable).Fields.Add(loField, lcField)
endcase
endscan
use in CoreMeta
endproc
enddefine

* If this is a field, add it to the appropriate table.

define class Table as Custom


add object Fields as Collection
enddefine

case CoreMeta.cRecType = F
lcTable = juststem(CoreMeta.cObjectNam)
lcField =
trim(justext(CoreMeta.cObjectNam))
loField = createobject(Field)
with loField
.DataType = CoreMeta.cType
.Length
= CoreMeta.nSize

define class
DataType
Length
Decimals
Binary
AllowNulls
Caption
enddefine

Field as Custom
=
= 0
= 0
= .F.
= .F.
=

code-focus

15

Event Binding in VFP 8


Visual FoxPro developers have been using an event-based methodology for a very long time. For most purposes, events are what drive the development effort. The user clicks a button, causing an event to fire, and the developer
writes code to react accordingly. All of this happens very transparently and without difficulty for either party. However, from a developers point of view, there also isnt much
flexibility in this approach. But in VFP 8, event handling is changing for the better.

megger@eps-software.com

Markus Egger is president of


EPS Software Corporation,
located in Houston, Texas. He
is also the founder of EPS Software Austria, located in
Salzburg.
He concentrates on consulting
in COM-based, object-oriented
development and Internet applications. He is an international
author and speaker, and is copublisher of Component Developer Magazine. He is also the
author of Advanced ObjectOriented Programming with
Visual FoxPro, from Hentzenwerke Publishing.
For the past several years,
Markus has received the
Microsoft MVP award.
Several applications he has
worked on (mostly as project
manager) have received
Microsoft Excellence Award
nominations.
He is the author of several
tools, such as GenRepoX
(public domain), the Fox Extension Classes, and Visual
WebBuilder.
A full bio can be found on the
web at: www.epssoftware.com/MarkusEgger

irst of all, dont worry! Things are still just as easy


and painless as they were in previous versions.
But Visual FoxPro 8 adds a whole lot of flexibility to the event mechanism. Before we delve into
the details of the new functionality, lets examine
what we had in the previous
version.

Events Explained
For Visual FoxPro developers,
an event and the code that
reacts to it are one and the
same thing. But thats actually
incorrect. An event is a very
different animal from the code
that runs when an event
occurs. Lets look at a simple
example.

the whole thing, Visual FoxPro realizes that the


code we wrote is meant to go with the button
object. Visual FoxPro can also look at the buttons
interface to see that the button might occasionally
raise a Click event. Therefore, VFP compiles our
code and links it to the button
by simply using a naming
Fast Facts
convention that says the
whenever there is a method
Event Handling in VFP 8 opens a
with the same name as an
new world to developers who need
event, that method is executed
to hook various events to
when the event occurs.

custom code. With the new


BINDEVENT() function, Microsoft
has handed over a fabulous
new capability to those who dare
to use it.

Lets assume we drop a button on a form. The


button is a Visual FoxPro standard control (base
class) that comes with a whole lot of functionality
that is exposed to the developer as properties,
methods, and events. This is known as the buttons
interface. By that we mean programming interface, and not the user interface.

What is interesting here is that we


can have more than one handler
for an event. In fact, we can add
as many different event handlers
to each event as we want!

Markus Egger

One of the most frequently used features of a


button is the Click event. When we double-click
this event in design mode in the property window,
Visual FoxPro opens the code window, showing
the Click() method. For this reason, most developers think that the Click() method and the Click
event are one and the same thing. Thats not the
case.
When we add code to the Click() method, we are
not really touching the buttons event at all.
Instead, we simply put code into a method that
goes with the buttons instance. When we compile

16 code-focus

Note that this fact is specific to


Visual FoxPro. If the designers
of VFP had decided that the
name of a click method would
be prefixed by On, then the
method that goes with the
Click event would have to be
called OnClick. (This, in fact, is a convention used
by other development environments). So the name
itself isnt really important. Whats important is that
there is some naming convention that allows the
compiler to automatically link our code to the
event. Otherwise, wed have to establish the link
manually, which would be a lot of work.
But sometimes, this may be desired! Perhaps we
would like to dynamically attach and detach event
handling code. Or perhaps we would like to handle
different events with the same event handler. And
thats where VFP8s new event binding comes in!

Manual Event Binding


So lets assume we have the following method as a
member of our form, and would like to bind it to a
button on our form:
FUNCTION ShowMessage()
MessageBox("Button Clicked")
ENDFUNC

We could do so by using the new BindEvent() function. We could use that function whenever and
wherever we want. In this example, the forms Init()
method might be the best place:
FUNCTION Init()
BINDEVENT(THIS.command1,"Click",;

The first two parameters define the source of the


event (object, plus name of the event), the second
pair defines the handling object and the method we
want to delegate the event to.
If you run this code inside a form and click the
button, you will see the message box firing. Note
that this doesnt influence the original click event at
all. You can check this by simply adding another
message box to the buttons Click() method. In that
case, the ShowMessage() method will fire first
(whenever a Click event is raised), and then the
Click() method will follow.
If you would like the sequence to occur the other
way around, with Click() firing first, you can indicate so by passing an optional 5th parameter to the
BindEvent() function:
BINDEVENT(THIS.command1,"Click",;
THIS,"ShowMessage",1)

Aside from the sequencing, what is interesting here


is that we can have more than one handler for an
event. In this case, we have our ShowMessage()
method as well as the default Click() method. And
it doesnt stop there! In fact, we can add as many
different event handlers to each event as we want!

Unbinding Events
Just like we bound event handlers to events, we can
also unbind events. So lets assume we want our
ShowMethod() to only handle the very first Click
event the button raises, but not subsequent ones. In
that case, we could add the following code to the
ShowMessage() method:
FUNCTION ShowMessage()
MessageBox("Button Clicked")
UnbindEvents(THIS.command1,"Click",;
THIS,"ShowMessage")
ENDFUNC

Of course, this assumes that the ShowMessage()


method is designed to be attached to THIS.
command1, and no other object. In real-life
scenarios, you may not want to do that, because
these types of dynamic event handlers generally are
very generic so they can be attached to different
objects. However, it would be easy enough to put
this code in other methods or event handlers as
well. We will also explore some generic ways to
discover current bindings later in this article.
Note that there also is a simpler version of
UnbindEvents() that receives one parameter (an
object reference):
UnbindEvents(THIS)

This will automatically unbind all event handlers on


the THIS-object (which would include our

From the VFP Team:

Note that some controls behave


slightly differently than others
when we manually bind to events
such as When() and Valid().

THIS,"ShowMessage")
ENDFUNC

ShowMessage() method). In addition (and this is


important to know), it will also unbind all event
handlers in other objects, that are binding to events
that may occur on the THIS object. Therefore, if
another object binds to the Activate event of the
form (for instance), that binding will be released as
well. While this is useful in some scenarios (perhaps
we want to release the THIS-object from memory),
it is also a rather barbaric approach, compared to
the surgical accuracy of the 4-parameter version.

Binding to Multiple Events


It is often desirable to bind one event handler to
multiple events. This is easiest explained through an
example: In many Visual FoxPro forms, developers
us a Validate() method to verify data entered in the
form. This method then may be triggered before the
data gets saved, or when a certain button is clicked,
or perhaps when the data is actually changed.
Either way, the Validate() method is called manually. With dynamic event binding however, that
method could be called whenever any of the data
changes by binding to the Valid events on the
controls of the forms. The following code illustrates
how to accomplish that task:
FUNCTION Init()
LOCAL loControl
FOR EACH loControl IN THIS.Controls
IF PEMSTATUS(loControl,"Valid",5)
BINDEVENT(loControl,"Valid",;
THIS,"Validate")
ENDIF
ENDFOR
ENDFUNC

Mike Stewart
Test Engineer,
Visual FoxPro Team
Microsoft
The new event binding features
in VFP 8.0 are probably one of
the most powerful new functions added to our latest
version. You can now hook any
method of any object to the
event of any VFP object,
including the ability to chain
multiple object methods
together for one event. This
allows you to extend the functionality of the code of any
event method to do anything
you want at runtime by simply
hooking on any object
method to that event method.
You can even raise an event on
any custom or base method of
a VFP object. The more you
work with event binding in VFP
8.0, the more youll find new
things you can do with VFP like
never before.

This simply iterates over all the controls on the


form the Init() method belongs to, checks if the
control has a Valid event, and if so, binds the
custom Validate() event handler method to that
event. This results in the Validate() method
handling all Valid events that may occur on any
control. Note that this doesnt influence any other
event handler (such as the standard Valid() method
on each control).
Note that some controls behave slightly differently
than others when we manually bind to events such
as When() and Valid(). Textboxes, for instance,
require that there is code in the actual Valid()
method before the manually bound code will fire.
This is an inconvenience, but it has to do with some
of FoxPros internal architecture. You can find more
information about these special cases in the Visual
FoxPro documentation.

code-focus

17

The ability to bind a single event handler to multiple


event sources is very convenient in a number of
scenarios. For instance, this technique can be used
to add debug or logging code to a project. The code
in Listing 1 shows a form class that has the ability
to log every button click that happens on the form.
(I am showing this in full source code in this
example, but of course this works very similarly in
visual classes.)

Listing 1: Logging button clicks on an entire form.


#DEFINE DEBUG .T.
DEFINE CLASS MyForm AS FORM
FUNCTION INIT()
#IF DEBUG
LOCAL loControl
FOR EACH loControl IN THIS.CONTROLS
IF loControl.BASECLASS = "Commandbutton"
BINDEVENT(loControl,"Click",THIS,"LogClick")
ENDIF
ENDFOR
#ENDIF
ENDFUNC
#IF DEBUG
FUNCTION LogClick
LOCAL laObjects(1), loEventSource
AEVENTS(laObjects,0)
loEventSource = laObjects(1)
WAIT WINDOW loEventSource.NAME
ENDFUNC
#ENDIF
ENDDEFINE

The code should be pretty


self-explanatory, perhaps
with the exception of the
call to AEVENTS(). Used
as shown in this example,
AEVENTS() will return a
reference to the object that
fired the event. This is
important
in
many
scenarios where an event
handler can dynamically
be bound to different
objects, yet a reference to
the object is required (such
as querying the object
name in our example).

Built-In Hooks

Hooks are a popular


mechanism
to
create
extensible software. Microsoft uses this mechanism
quite a lot in Visual
FoxPro itself. A good
example for this is the
Class Browser. It exposes
an interface that allows
developers to write AddIns
by using the browsers
proprietary mechanism to hook its events and
methods to those written by the developer. The
problem with this approach (besides the proprietary
nature of the mechanism) is that Microsoft has to
write all those hooks manually. This is a lot of
work, which is the main reason this type of mechanism isnt used in all the software written in VFP. In
the typical everyday project, developers simply dont
have the time to add this functionality. Therefore,
most applications do not offer hooks.
With dynamic event binding, however, we get this
type of functionality automatically. Every application that fires events can be extended using manual
event binding. This is even true for internal VFP
objects, such as the VFP main screen. In older
versions of Visual FoxPro, it was often not possible
to use the default screen (VFPs main window) as
the application window, since one had very little
control over this object. For instance, you could not
react programmatically when the screen was
resized.
Now, however, we can hook this event and react
to it the same way we otherwise would implement
in subclasses.

18 code-focus

Events vs. Methods


One of the little known facts is that Visual FoxPro
internally handles different events in different ways.
This has to do with Visual FoxPros history. Some
events, such as When() and Valid(), go way back to
pre-Visual versions, while others were introduced
with Visual FoxPro 3.0. The difference is that some
events are true events, while others are just simple
methods that are being called when certain things
happen, which makes them look like true events for
most purposes.
For manual event binding, however, the difference
is significant. It would be very difficult for most
developers to first figure out whether an event is a
true event or just an automatically fired method. For
this reason, the Visual FoxPro team built the
BindEvent() function so that it also allows you to
bind any type of handler method to any other
method. This way, we can bind to all sources of
events equally.
This has the convenient side-effect that we can bind
handler methods to all types of methods, no matter
whether they are internal methods or custom
methods defined by the developer. For instance, we
could add a method called SomeMethod to the
form. We can then add another method called
SomeOtherMethod to the same form and bind
that method to the first method:
FUNCTION Init
BindEvent(THIS,"SomeMethod",;
THIS,"SomeOtherMethod")
ENDFUNC

Now, whenever SomeMethod()


SomeOtherMethod() fires, as well.

gets

called,

Sometimes, this behavior may not be desired. For


instance, one might want to bind to the Click()
event, but not to the Click() method. This can be
done using an optional flag:
BindEvent(THIS.cmdOne,"Click",THIS,"Handler",2)

The 2 in the fifth parameter indicates that we do


not want to bind to the Click() method, but only to
the event. If someone clicks on the button, the
handler method will fire. However, the following
code would not invoke the handler method:
THIS.cmdOne.Click()

Of course, this only works because Click() is a true


event (unlike Valid(), for instance).

Conclusion
BindEvent() is a very powerful new feature. It is
probably not a feature that every developer has
been waiting for, but those of you who have a need
for this feature need it badly.
If you have any questions about this feature, feel
free to send me an email.
Markus Egger

Introducing the
CursorAdapter Class
One of the most exciting new features of Visual FoxPro 8 is the
CursorAdapter class, which provides a common interface for
working with data from many different sources. Chuck takes you with
him on an adventure in exploring how to use CursorAdapter to change the way you
relate to data in VFP 8, whether native tables, ODBC, OLE DB, or XML.

Chuck Urwiler
chuck@eps-software.com

Chuck Urwiler is a Senior


Developer and Consultant with
EPS Software Corporation,
where he develops applications
using Visual FoxPro, VB.NET,
ASP.NET, XML, and SQL
Server. Over the past several
years, Chuck has provided
training for thousands of developers as well as designing and
implementing mission-critical
applications using Microsoft
tools and technology.
He continues to share his
knowledge with the developer
community through presentations at user groups, Microsoft
seminars, and Developer
Conferences for VFP, SQL
Server, and .NET. Chuck is a
co-author of Building
Client/Server Applications with
VFP and SQL Server by
Hentzenwerke Publishing and
has served as a technical editor
on several other books
regarding SQL Server, MSDE,
and e-Commerce. Chuck is a
Microsoft Certified Solution
Developer (MCSD) and a Certified Database Administrator
(MCDBA).

20 code-focus

ith the introduction of the CursorAdapter


class in VFP 8, the Fox team has finally made
a significant change in the way a VFP application accesses data, whether it is native or from a
remote data source. Additionally, the setup of
CursorAdapter classes will be somewhat familiar to
those who are well-versed in the behavior of views
and SPT, as well as the alternative data sources of
using ADO RecordSets or XML documents.

easier to devise a strategy for building data classes


using the CursorAdapter Class.

Your First CursorAdapter Class


Like any other class, the best way to learn how to
use it is to see how one is created. To keep the complexity low for these first examples, lets start by accessing VFP native tables with a CursorAdapter
class. This is very much like using a Local View to
retrieve data from VFP tables. Later on in this article, well use other CursorAdapter classes to connect to SQL Server data, OLE DB data, and an
XML document.

The CursorAdapter class is unique in that it is the


first VFP base class to provide conversion between
native VFP cursors and ODBC, ADO or XML data
sources, all within a single class. In other words, the
ability to translate an ODBC data stream, an ADO RecordSet,
or an XML document into a
Fast Facts
VFP cursor is all built into the
CursorAdapter class.
The CursorAdapter class is one of

the most impressive

First, you have two ways to


create a CursorAdapter. You
can use the Data Environment
builder or you can build the
class by hand through a program or the class designer. This
example will use the Data Environment builder; later examples will be built by hand.

You can probably tell already


accomplishments of the VFP 8
that the CursorAdapter is an efdevelopment
team. It will change
fective replacement for the local
the
way
many
developers relate to
view and remote view technolotheir various data sources.
gy from earlier versions (Note:
If youre not familiar with the
neither of these features has
enhancements VFP 8 brings to
been removed from VFP 8). But
the Data Environment, you
in some cases, it also replaces
might think that using a builder in the DE to create
SQL Pass Through, and also reduces the need to
a CursorAdapter would only be useful within a
work directly with ADO and XML in your code.
Form, not a class. However, the DE has been enhanced in VFP 8 so it can be instantiated without
One key advantage to the CursorAdapter is for situthe presence of a form!
ations where you need to connect to more than one
data source from within the same application. For
Start by creating a new Data Environment class
example, if your application retrieves most of its dawith the create class command. Be sure to select the
ta from SQL Server, but also needs to work with a
Data Environment class in the Based On drop
handful of XML documents, the CursorAdapter can
down. Name the class deTest and store it in a class
be used in both cases to make all the data appear to
library called Tests.vcx. Once the class appears in
your application as a set of VFP cursors.
the class designer, right click on the Data Environment and select Builder from the drop down. This
Another example might be a situation where the data
brings forward the Data Environment builder.
is currently stored in VFP tables, but future plans are
to move to a database server, like SQL Server or OrIn the data source type drop down, note the availacle. You would build a set of CursorAdapter classes
able options. Since this first example will connect to
first for VFP and then, if necessary, replace these
native VFP tables, choose Native. Once selected,
classes with SQL Server equivalents when necessary.
use the ellipsis button to choose the Northwind
database (default location is c:\program files\miBut, since we must walk before we can run, lets
crosoft visual foxpro 8\samples\northwind\northtake a basic tour through the CursorAdapter class
wind.dbc).
and its different incarnations. After that, it will be

Next, click the Cursors page, which is initially empty. Under the list box, choose the New button to
create a new CursorAdapter class with the
CursorAdapter Builder. Initially, you will see the
Properties page, providing options for choosing the
name of the class and the alias of the cursor created
by the class.

Near the bottom of the data access page is the


buffer mode override setting, which allows you to
override any associated forms buffer mode setting.
Generally, you want to use the optimistic table
buffering mode unless you have a specific reason to
use the row buffering mode. Set this to Optimistic
Table Buffering for this example.

Finally, the Break on error setting at the bottom


of the page controls how errors are handled by the
CursorAdapter class. The default setting specifies
that the class will trap its own errors and allow you
to capture them with the AERROR function. Enabling this setting will cause a VFP error to occur
whenever any problems occur within the Cursor
Adapter class. This means that you will need to use
the ON ERROR command or Error event of the
class to trap unhandled exceptions. Generally, you
will want to leave this setting off so that your code
can quietly handle any exceptions that may occur.

The ability to translate an ODBC


data stream, an ADO RecordSet,
or an XML document into a VFP
cursor is all built into the
CursorAdapter class.

Be sure to provide an alias that differs from the


table name to avoid confusion with the base table.
In this case, use caCustomer as the class name and
cCustomer as the alias. You should also check the
Use DataEnvironment data source option since
you want this class to use the same data source as
the data environment. Note that you could have a
different data source for the CursorAdapter, allowing you to mix data sources between different classes (such as using ODBC for one class and XML for
another).
To specify how the CursorAdapter will retrieve data from the source, use the Data Access page of the
builder. Click the Build button to activate a command builder dialog, where you can select the
field(s) of interest for your cursor. For this example,
select the Customers table from the Table drop
down list, and then select the Customers.* option
in the list box below. Click the single right-facing arrow to move the selection, and then press OK. This
will build the following select command for you:
select CUSTOMERS.* from CUSTOMERS

If you wish to add filters, joins, or other clauses to


the query, you can type them directly into this edit
box. However, if you wish to parameterize the
query, there are several options, covered later in this
article. For now, lets add a WHERE clause so that
it looks like the following:
select CUSTOMERS.* from CUSTOMERS where
companyname like C%

This will make it easy to tell the difference between


the base table and the resultant cursor, since only a
few records match this Where clause.
The schema has been built for you in the second edit box. It is usually best to take a minute and verify
that the schema matches your expectations before
proceeding. The data fetching properties at the bottom of this page relate mostly to how the class deals
with remote data fetches, and do not apply when
using VFP as a data source. Well leave these at
their defaults and cover them in more detail later.

The final page (labeled Auto Update) configures


how changes are applied to the base table(s) of the
class. For the most basic updates, choose the Autoupdate and Update all fields checkboxes. This
will direct the CursorAdapter class to automatically
build the appropriate Update, Insert or Delete statements for any changes that are made to the data in
the cursor. However, you must still choose the primary key field(s) for the cursor, so that these statements know how to uniquely identify the base table
records. For this example, the CustomerID field is
the primary key, so check the box in the column under the key.
For the time being, leave all of the other settings at
their defaults. These settings are explored later on in
this article. To finalize your selections in the
CursorAdapter builder, click the OK button to return to the DataEnvironment builder.

From the VFP Team:

Alan Griver
Group Manager,
Visual Studio Data Team
Microsoft
One of the many areas we
enhanced in VFP 8.0 is access
to different data sources. The
new CursorAdapter class is the
best way to achieve unified data
access to native VFP tables,
XML, ADO.NET, SQL Server,
and ODBC or OLE DB data
sources. This class combines
many of the great remote view
and SQL PassThrough capabilities that youre used to from
prior versions of VFP while
providing many new capabilities. Based on feedback weve
received from VFP developers,
the CursorAdapter class is one
of the most popular new
features in VFP 8.0.

At this point, you should see the caCustomer class


listed on the left, and details on the right. If you decide to make modifications to this class, you can return to the DataEnvironment builder at any time,
select the desired CursorAdapter class, and click the
Builder button.

Accessing VFP data


At this point, you can try out the Data Environment
to see if it retrieves the data specified by the select
command in the CursorAdapter. Using the command window, instantiate the DE class and invoke
the OpenTables method:
lo = NewObject("deTest","Tests.vcx")
? lo.OpenTables()
BROWSE

When the OpenTables method is fired, the


CursorAdapter is instructed to fill its cursor with the
results of the Select command that you specified in
the builder. When you BROWSE, you will see only
the customer records that have a CompanyName
which starts with C (normally, five records
match).

code-focus

21

Youll have to ensure that any


CursorAdapter object variables
stay within scope for as long as
you intend to access the
associated cursor.

One special behavior that comes with the


CursorAdapter is that the cursor is coupled to the
object instance; therefore, if you destroy the object
reference to the CursorAdapter class, you will also
lose the cursor and its contents. This means that
youll have to ensure that any CursorAdapter object
variables stay within scope for as long as you intend
to access the associated cursor.

Modifying VFP Data

Is the cursor a
member object of the
CursorAdapter?
A possible misconception you
may have is that the cursor is a
member object of the CursorAdapter. It is not. Instead, it is
just like any other VFP cursor in
that it is not encapsulated
within any object. This means
that you cannot pass a CursorAdapter object reference
between two applications (or
application tiers) and expect the
cursor to follow instead, it
will exist only where the cursor
was created.
Therefore, if you wish to pass
the data retrieved by a
CursorAdapter as a parameter
to another application or
process, youll have to convert
the data into something like an
XML document or ADO
recordset for the data to be
properly marshaled to the
receiving end.

Now, lets see if the cursor allows updates and posts


them properly to the base table. Try the following
lines of code in the command window:
REPLACE contactname WITH My Name Here
?TABLEUPDATE()
SELECT customers
BROWSE

Once you browse the Customers alias, you see the


base table and should be positioned on the record
that you modified. If you didnt move the record
pointer before issuing the Replace statement, the
record with CACTU as the customer ID was modified. Regardless of which record you modified, this
proves that the CursorAdapter is updatable and that
the updates are being sent properly to the base
table.

Under the Hood

All of the properties that contain See Init are populated at run time by the code generated for the Init
method. That code is shown in Listing 1.
This is probably the most educational place to look
after the builder is finished to see how the properties have been set. Note that you can change these
values here or through the builder. However, by
changing them here, you run the risk of breaking
functionality, as your property changes are not verified as they are within the builder.

Lets open the Data Environment class that you just


tested to see what the builder did for us. This is not
just an exercise it is a great way to learn how to
properly configure a CursorAdapter class should
you decide to make your own classes outside of the
Data Environment.

Regardless, you can see in the Init() code how the


SelectCmd property is specified, as well as the KeyFieldList, UpdatableFieldList, and the UpdateNameList. Pay particular attention to the format of
the UpdateNameList property this property lists
each field from the cursor and its corresponding
field (with table name) in the base table.

While the Data Environment has a few property


changes and a method, were actually not interested

When creating your own CursorAdapter classes


from scratch, you may be tempted to leave out the

Property or Method

Value

Description

Alias
AutoOpen()

cCustomer
<code>

BufferModeOverride

Flags
Init()

5 Optimistic
Table Buffering
CUSTOMERID
C(5),
0
<code>

Sets the alias name of the cursor.


Called by the DE automatically to set up a status property
used by the Init method if necessary.
Forces the cursor to have the specified buffering mode.

KeyFieldList

See Init

Name
SelectCmd

caCustomer
See Init

Tables
UpdatableFieldList
UpdateNameList

CUSTOMERS
See Init
See Init

UseDEDataSource

.T. True

CursorSchema

Specifies the structure of the resultant cursor.


Used only by XML data sources
Establishes the value of the SelectCmd, KeyFieldList,
UpdateNameLIst, UpdatableFieldList properties.
Specifies the field(s) that uniquely identifies records in
the base table. Set by the Init method code.
Specifies the SQL Select command used to retrieve data
from the data source.
Lists the tables used by the CursorAdapter.
Specifies the list of fields that are updatable.
Specifies a mapping of cursor fields to the field names in
the base table.
Specifies whether the parent Data Environment controls
the data source of the cursor.

Table 1: The properties set by the CursorAdapter builder.

22 code-focus

in those changes. Instead, look in the property


sheets object drop-down list and select the caCustomer class to see the settings that are required to
make the CursorAdapter class work. Table 1 summarizes the changes made by the builder and what
each PEM does.

code-focus

23

Listing 1: The CursorAdapter Init() method generated by the builder


local llReturn
do case
case not pemstatus(This, __VFPSetup, 5)
This.AddProperty(__VFPSetup, 0)
case This.__VFPSetup = 2
This.__VFPSetup = 0
return
endcase
llReturn = dodefault()
*** Setup code: DO NOT REMOVE
***<SelectCmd>
text to This.SelectCmd noshow
select CUSTOMERS.* from CUSTOMERS where CompanyName like C%
endtext
***</SelectCmd>
***<KeyFieldList>
text to This.KeyFieldList noshow
CUSTOMERID
endtext
***</KeyFieldList>
***<UpdateNameList>
text to This.UpdateNameList noshow
CUSTOMERID CUSTOMERS.CUSTOMERID, COMPANYNAME CUSTOMERS.COMPANYNAME, CONTACTNAME
CUSTOMERS.CONTACTNAME, CONTACTTITLE CUSTOMERS.CONTACTTITLE, ADDRESS
CUSTOMERS.ADDRESS, CITY CUSTOMERS.CITY, REGION CUSTOMERS.REGION, POSTALCODE
CUSTOMERS.POSTALCODE, COUNTRY CUSTOMERS.COUNTRY, PHONE CUSTOMERS.PHONE, FAX
CUSTOMERS.FAX
endtext
***</UpdateNameList>
***<UpdatableFieldList>
text to This.UpdatableFieldList noshow
CUSTOMERID, COMPANYNAME, CONTACTNAME, CONTACTTITLE, ADDRESS, CITY, REGION, POSTALCODE, COUNTRY, PHONE, FAX
endtext
***</UpdatableFieldList>
*** End of Setup code: DO NOT REMOVE
if This.__VFPSetup = 1
This.__VFPSetup = 2
endif
return llReturn

table name in this listing. However, without this exact format, your updates will fail, but without errors.
Ill reiterate this point later when creating a class
without the builder.
Earlier I stated that the CursorAdapter, using the
Native data source, is essentially a replacement for a
Local View. If you have ever built a local view, you
probably can see the similarities: a SQL Select statement is constructed, you specify which fields you
wish to be updatable, you specify the field or fields
that comprise the primary key, and then let VFP do
the rest. Once you retrieve the data in the cursor,
you can use TableUpdate() to send the changes back
to the base table, and VFP automatically builds the
necessary Update, Insert or Delete statements to
carry out the modifications.
As an example, recall the earlier Replace statement
that changed the value of the Contact field in the
cCustomer alias. Upon issuing the TableUpdate
statement, VFP automatically generates (and sub-

24 code-focus

mits) the following Update command to attempt the


modification:
UPDATE customers ;
SET CONTACTNAME=ccustomer.contactname ;
WHERE ;
CUSTOMERID=OLDVAL(customerid,ccustomer);
AND ;
CONTACTNAME=OLDVAL(contactname,ccustomer)

VFP was able to generate the WHERE clause by


referencing the KeyFieldList property of the
CursorAdapter as well as parts of the UpdateNameList property. It also takes into account which
field was changed and adds in the necessary clauses
to ensure that you dont attempt to update a record
that has been changed by someone else. Note that
this is because we left the WhereType property at its
default of key fields and any modified fields.

Handling Errors
Obviously, not everything will go as planned when
trying to update data from the CursorAdapter. As
you well know, TableUpdate can fail for a variety of
reasons, such as an update conflict or a record lock.
Do you have to do anything special with the
CursorAdapter class to detect these problems? The
answer is, it depends.
Lets create a simple update problem by locking the
record that the CursorAdapter is attempting to
update. If the class designer is still open, close it.
Then, instantiate the deTest class with the NewObject function, just as you did above, and call the
OpenTables method. Browse the cursor so that you
can see the data, but dont change anything yet.
Now open a second instance of VFP 8 so you can
lock the record. Execute the following lines in the
command window to lock the record that youll
attempt to update:
OPEN DATABASE (HOME(2)+"Northwind\northwind.dbc")
USE customers
LOCATE FOR customerid = CACTU
?RLOCK()

You should get a return value of .T. to show that the


record is actually locked by this instance of VFP.
Return to the first instance of VFP and attempt the
following code from the command window:
REPLACE contactname WITH updated
SET REPROCESS TO 2 SECONDS
?TABLEUPDATE()

In this case, TableUpdate returns .F., showing that


the record lock prevented the update from
succeeding. If you issue a call to AERROR() and
display the contents of the resultant array, you will
see the error message Record is not locked. This
means that you can handle such errors in the same
way as if you were working directly with the
buffered table and not a cursor.

Unfortunately, not all expected errors will behave


this way. Of particular note is the Update Conflict,
where an update made by one user attempts to
overwrite the changes made by another user. To see
this in action, issue the following commands in the
current instance of VFP (where the CursorAdapter
is being used):
?TABLEREVERT(.T.)
REPLACE contactname WITH client 1

Now switch over to the second instance and issue


the following commands:
CLOSE DATABASES all
OPEN DATABASE (HOME(2) + "Northwind\northwind.dbc")
USE customers
LOCATE FOR customerid = CACTU
REPLACE contactname WITH client 2
BROWSE

Return to the first instance, and attempt to update


the changes with TableUpdate:
?TABLEUPDATE()

In this case, TableUpdate incorrectly returns a .T.,


leading you to believe that the update was
successful! However, it was not, and this can be
proven by invoking the CursorRefresh() method of
the CursorAdapter, as in the following code:
?TABLEREVERT(.T.)
?lo.caCustomer.CursorRefresh()

The CursorRefresh method tells the CursorAdapter


to re-execute the SelectCmd and retrieve the latest
data from the base table. Examination of the
ContactName field shows that the value was never
updated from the CursorAdapter!
The easiest way to solve this problem is to take
advantage of the AfterUpdate method on the
CursorAdapter. This method is invoked after the
TableUpdate attempts to save the changes to each
record in the result set. Note that this method is
invoked only for a change to a current record. If the
record is new or the record is deleted, then the
AfterInsert or AfterDelete methods would fire.
The AfterUpdate method captures several parameters, including the original field state, whether

changes were forced, and the text of the commands


that were used for the update. The last parameter,
lResult, is the most critical for our current topic, as
it tells us whether the update was deemed a success
by the updating process.
The other feature to use to solve the update conflict
problem is the system variable _TALLY, which tells
how many records were affected by the last operation. Therefore, if lResult is true, but _TALLY is zero,
then no records were updated, and you can assume
that the problem in this case was an update conflict.
In summary, the simple way to solve this problem is
to add the following code to the AfterUpdate
method on the CursorAdapter class:
LPARAMETERS cFldState, lForce, nUpdateType,
cUpdateInsertCmd, cDeleteCmd, lResult
IF lResult AND _TALLY = 0 THEN
ERROR 1585 && update conflict
ENDIF

What is interesting here is that you will not see the


error message appear on your screen; instead, the
message is trapped by the TableUpdate call, forcing
you to use the AError function to see the cause of the
update failure. This occurs because the BreakOnError property was left at its default of False,
meaning that errors should not cause a break. If you
were to set this property to True, then the Update
Conflict error message would appear, or if specified,
your ON ERROR handler would be triggered.
This issue of update conflicts is by design for the
CursorAdapter since there is no easy way for VFP 8
to automatically detect this problem. Therefore, this
code (or similar) will probably end up in your foundation CursorAdapter classes when going against
native data sources.

CursorAdapter with ODBC


Now that youve seen the basics, lets move forward
to see how things change when using SQL Server as
the back end instead of VFP. Well start with using
the ODBC driver from VFP to access the Northwind database on SQL Server. Also, lets build this
CursorAdapter from scratch so that every aspect
of the class is visible.
First, create a new class in the class designer with
the following command:

CREATE CLASS caODBC OF tests as CursorAdapter

You can use TableUpdate() to


send the changes to the base
table, and VFP automatically
builds the necessary Update,
Insert or Delete statements to
carry out the modifications.

The most important property to set at this point is the


DataSourceType property. Since were attempting to
connect to SQL Server via ODBC, set this property
to ODBC. When set in this fashion, the DataSource
property expects a valid connection handle, which
can be created through the SQLConnect or
SQLConnectString functions. In either case, these
functions should be invoked through the Init method
of the CursorAdapter class using the following code:

code-focus

25

LOCAL lcConnStr, lnConn


** string assumes trusted connection (integrated
security)
lcConnStr = "Driver=SQL
Server;Server=(local);DATABASE=Northwind"
lnConn = SQLSTRINGCONNECT(lcConnStr)
IF lnConn > 0 THEN
THIS.DATASOURCE = lnConn
ELSE
** unable to connect
ENDIF

The connection string assumes that you are using a


trusted connection to SQL Server; if you are using
SQL Server security instead, add the uid= and
pwd= strings to the connection string to specify
the username and password for the connection. In
either case, the return value is the connection
handle, or a negative value if an error occurred.
This connection handle is assigned to the DataSource property so that the CursorAdapter knows
where to pass statements.

The next step is to build a SelectCmd so that the


CursorAdapter knows what data it is acquiring from
the data source. The best place to do this is also in
the Init method, since the property sheet does have
limitations on how long a string you can provide.
Add the following line to the Init method, after the
code that sets the DataSource property, to retrieve
the list of Customers that have a Company Name
that starts with C:
This.SelectCmd = "SELECT " + ;
"CustomerID, CompanyName, ContactName, " + ;
"Address, City, Region, Country " + ;
"FROM Customers WHERE CompanyName LIKE C%"

Next, you need to tell the CursorAdapter to fill the


associated cursor with a call to the CursorFill
method. You could leave out this call and invoke it
manually from outside of the class, or place it in the
Init method so it automatically fills upon instantiation. Simply call This.CursorFill() after the
SelectCmd is populated in the Init method.
Finally, you should add a bit of code to the Destroy
method of the class, to drop the server connection
once the object is removed from memory. Without
this code, every new instance will create a new
connection to the server, and never release it:

Listing 2: The modified version of the Init() method


LOCAL lcConnStr, lnConn, llRetVal
WITH This
lcConnStr = "driver=sql server; server=(local) " + ;
"database=northwind"
lnConn = SQLSTRINGCONNECT(lcConnStr)
llRetVal = .T.
IF lnConn > 0 THEN
.DataSource = lnConn
.SelectCmd = "SELECT CustomerID, CompanyName, ContactName, "+ ;
"Address, City, Region, Country "+ ;
"FROM Customers WHERE CompanyName LIKE C%"
IF NOT .CursorFill() THEN
** unable to fill.
llRetVal = .F.
ELSE
.Tables = "customers"
.KeyFieldList = "CustomerID"
.UpdatableFieldList ="CompanyName, ContactName, Address, "+ ;
"City, Region, Country"
.UpdateNameList= "CustomerID Customers.CustomerID, " + ;
"CompanyName Customers.CompanyName, ContactName " + ;
"Customers.ContactName, Address Customers.Address, " + ;
"City Customers.City, Region Customers.Region, " + ;
"Country Customers.Country"
STORE .T. to .AllowDelete, .AllowInsert, .AllowUpdate
ENDIF
ELSE
** unable to connect
llRetVal = .F.
ENDIF
ENDWITH
RETURN llRetVal

26 code-focus

IF this.DataSource > 0 THEN


SQLDISCONNECT(this.DataSource)
ENDIF

With these changes, you have a functional


CursorAdapter class that produces a non-updatable
cursor. Still, it may be a good time to test the class,
to ensure that it can be instantiated and that it
retrieves data properly, before allowing it to be
updatable. Test it with the following code:
lo = NEWOBJECT("caODBC","tests")
BROWSE

Note that you didnt have to invoke an OpenTables


method like you did with the Data Environment.
This is because you added the CursorFill method
directly to the Init method, causing the class to
automatically fill the cursor upon instantiation.

Updating ODBC Data


To make this class updatable, you have to provide
correct values for the Tables, KeyFieldList, UpdatableFieldList, and UpdateNameList properties. Also
set the AllowInsert, AllowUpdate, and AllowDelete
properties to True, to ensure that the automatic
updating feature is properly activated. Once again,
the best place to make these changes is through
code in the Init method. The modified version of
the Init method appears in Listing 2.
Before closing the class designer, you may also want
to change the BufferModeOverride property to 5
Optimistic table buffering so automatic updates do
not occur when moving the record pointer.
To test the updatability of the CursorAdapter,

instantiate it, browse the cursor, make a change,


and then issue TableUpdate. To ensure the changes
were applied, call the CursorRefresh method of the
CursorAdapter object and browse again.

RaisError. This can be done with the following code


in BeforeUpdate:
LPARAMETERS cFldState, lForce, nUpdateType, ;
cUpdateInsertCmd, cDeleteCmd

Handling ODBC Errors


As with the native CursorAdapter, most errors are
trappable in the traditional way test the return
value of TableUpdate and, in case of failure, use
AError to determine the cause. Unfortunately, the
detection of an update conflict is also a problem for
the ODBC type CursorAdapter.

CursorAdapter vs.
Local View
One advantage that a
CursorAdapter has over the
Local View is the lack of a DBC.
Since the view is a DBC object,
it can only be contained within
the DBC. Views can be a
problem in applications where
lots of users attempt concurrent access to the same view
VFP acquires a short lock on
the view record in the DBC, and
sometimes this presents
unexpected errors. Without the
need for a DBC, CursorAdapters
will never suffer from this
problem.

While the solution for the native CursorAdapter


was to raise an error in the AfterUpdate method,
this wont be as effective for the ODBC
CursorAdapter since were not expecting VFP
errors, but ODBC errors, when an update fails.
Therefore, the best answer is to either use a stored
procedure (covered later) or add a little more code
to the update statement as it is sent to the server.
Recall that the solution for the native
CursorAdapter was checking _TALLY to see if any
records were updated. The solution here for ODBC
is similar, but we cant use _TALLY since it isnt
reliably correct for remote data. Instead, we can use
SQL Servers @@Rowcount system function to
determine if any records were updated.
If you were writing a T-SQL batch of statements to
update a record, you might write code similar to the
following:
--@custID and @oldContact set by earlier code or
parameters
UPDATE customers
SET ContactName = @newContact
WHERE CustomerID = @custID
AND ContactName = @oldContact
IF @@ROWCOUNT = 0
RAISERROR(Update failed.,16,1)

The RaisError T-SQL function causes VFP to


receive an ODBC error (number 1526), passing
the error message as specified in the first parameter (the other two parameters indicate the
severity and state of the error). In this case, RaisError is invoked when @@Rowcount = 0, meaning
that the previous T-SQL statement did not affect
any records.
Where this all fits into the current discussion is
that you can use the BeforeUpdate method of the
CursorAdapter to modify the statement that is sent
to the server on an update. While the BeforeUpdate method receives five parameters, the last two
(cUpdateInsertCmd and cDeleteCmd) are interesting in that they are passed by reference. This
allows you to change the commands before they
are sent to the data source.
In our case, wed like to use this method to append
the test for @@Rowcount and subsequent call to

28 code-focus

IF nUpdateType = 1 THEN
cUpdateInsertCmd = cUpdateInsertCmd + ;
" if @@ROWCOUNT = 0 "+ ;
"RAISERROR(Update Failed due to update " + ;
"conflict.,16,1)"
ENDIF

Now, for every row that is sent to the back end, this
code will test to see if the row was updated. If not,
VFP will receive the error, TableUpdate will fail,
and AError will show the usual 1526 error with the
message text as specified.
There are two problems with this approach. First,
this is a specific fix for SQL Server; for other ODBC
data sources (such as Oracle), this code will not
work. Second, this error message is very generic as
it always generates the same VFP error number, and
makes proper error handling from VFP a bit difficult. This issue can be mitigated somewhat by
creating custom error messages on the SQL Server,
each with their own unique error number.
Another way to improve upon this solution is to use
Stored Procedures to perform the updates instead of
letting VFP build and pass an ad-hoc query to the
server. Of course, the tradeoff of adopting the stored
procedure approach is that you lose the benefit of
having VFP automatically handle the updates.

Parameterization
As a side note, you have now seen one way to parameterize the commands for a CursorAdapter class.
In essence, every event that occurs in the class has
a set of Before and After methods, such as BeforeUpdate and AfterUpdate. However, there is no
BeforeSelect or AfterSelect instead, these are
called BeforeCursorFill and AfterCursorFill, since
the cursor is filled with the result of the SelectCmd.
The BeforeCursorFill method receives three parameters, and expects a Boolean return value. The
first parameter, lUseCursorSchema, specifies
whether the CursorSchema property controls the
construction of the resultant cursor or not. The
second parameter, lNoDataOnLoad, is similar to
the NODATA clause on views, where the schema is
retrieved but no data is actually passed from the
data source.
For the current discussion, the third parameter,
cSelectCmd, is of primary interest. It is also
passed by reference (like the cUpdateInsertCmd
parameter of BeforeUpdate) and is initially populated with the current setting of SelectCmd.
However, if you change the value of this parameter, it does not change the value of the

For example, imagine that you have set a


CursorAdapter objects SelectCmd to the following
statement:
SELECT CustomerID, CompanyName, ContactName, City,
Region, Country FROM Customers

Upon calling the CursorFill method of the


CursorAdapter, the cSelectCmd parameter of the
BeforeCursorFill method would contain this value.
Now imagine that you have the following code in
this method:
cSelectCmd = cSelectCmd + ;
" WHERE CompanyName LIKE " + ;
this.cCompanyName + "%"

This would cause the actual Select command to


always contain the WHERE clause as specified by
the code and the current value of this.cCompanyName (a user-defined property). And since it
doesnt modify the original value of SelectCmd, you
dont have to include any special coding to ensure
that you dont get two WHERE clauses in the
submitted select command.

VFP will automatically convert


the ADO RecordSet into a VFP
cursor for us, and will also
handle the updating.

SelectCmd property; instead, it modifies what is


passed to the data source, for as long as the object
exists.

Essentially, this section is all about replacing the


automatic generation of Update, Insert, and Delete
commands with calls to stored procedures on the
data source. This means that youll be dealing with
the UpdateCmd, InsertCmd, and DeleteCmd properties, and assumes that the Northwind database on
your SQL Server already has stored procedures in
place for performing these functions (they are not
provided in the sample database).
As an example, lets take a look at the complete
code for a simplified stored procedure you can use
to update the ContactName field in the Customer
table for the Northwind database:

Parameterization, Part II
If you have used views or SQL Pass Through in the
past, then you are probably familiar with parameterization by using the ? character in front of a
variable. As you might suspect, this feature still
works in the CursorAdapter. The following example
code shows how you can use a parameter in the
SelectCmd property of a CursorAdapter:
This.SelectCmd = "SELECT * FROM Customers " + ;
" WHERE CompanyName like ?lcMyVar "
lcMyVar = C%
This.CursorFill()

It is critical to ensure that the variable lcMyVar is


populated before the CursorFill method is invoked.
Otherwise, you are prompted for the value by VFP,
something a user should never see.
You can also use a property of the CursorAdapter
as the parameter, instead of a local variable. The
advantage, of course, is that the property will persist
as long as the object does, and you could even
provide a set of Access/Assign methods to ensure
the assigned value meets certain criteria.

Using Stored Procedures


Above, it was suggested that using stored procedures would be a good way to get around the limitations of handling errors. With that in mind, lets
explore the approach of using stored procedures
with an ODBC-based CursorAdapter so we can get
a feel for how much work is involved in manually
handling the updates for a CursorAdapter class.

code-focus

29

Batch Updates
Let me give you a few words of
caution about our OLE DB
update code. If you have previously used ADO, you may be
used to setting the LockType
property to adLockBatchOptimistic instead of adLockOptimistic,as we did in our OLE DB
update example. However, if
you set it differently, you must
manually invoke the UpdateBatch method of the RecordSet
object.
You can perform batch
updates with an ADO-based
CursorAdapter, but you must
set the RecordSets LockType
property to adLockBatchOptimistic and use Optimistic Table
buffering. Then, allow your user
to make the batch of changes
to the cursor. When you wish
to update the data source, you
first call TableUpdate, which
updates only the RecordSet and
does not post the changes to
the back end. Once TableUpdate succeeds, then you must
invoke the UpdateBatch method
of the RecordSet to actually
update the data source.

--T-SQL code, not VFP


CREATE PROCEDURE UpdateCustomerContact (
@CustomerID nchar (5),
@ContactName nvarchar (30),
@oldContact nvarchar (30)
)
AS
IF @CustomeriD IS NULL
RAISERROR(CustomerID is a required
parameter,16,1)
ELSE
UPDATE Customers
SET ContactName = @contactName
WHERE CustomerID = @customerID
AND ContactName = @oldContact

To save space, this procedure is lacking the full


error handling code that you would normally
include. Regardless, there is enough code here to
illustrate how to perform an update with the
CursorAdapter class.
Fortunately, establishing the UpdateCustomerContact procedure as the Update command is as easy as
overriding the BeforeUpdate method with the
following code:
LPARAMETERS cFldState, lForce, nUpdateType, ;
cUpdateInsertCmd, cDeleteCmd
cUpdateInsertCmd = ;
"EXECUTE UpdateCustomerContact " + ;
EVALUATE(this.Alias+".CustomerID") + "," +;
ALLTRIM(EVALUATE(this.Alias+.ContactName))+ ;
"," + ;
OLDVAL(contactname,this.Alias)+""

Similar code should also be written for the


BeforeInsert and BeforeDelete methods, so that
they also call stored procedures instead of ad-hoc
queries. For the sake of space, Ill leave that code as
an exercise for the reader.

CursorAdapter with OLE DB


Our next task is to see how to use OLE DB with the
CursorAdapter class, and to compare it to how weve
used Native and ODBC data sources. OLE DB technology is more capable than ODBC, and may
provide access to more types of data sources than
ODBC. The CursorAdapter uses OLE DB by
hooking into the objects of ADO, which is the standard COM wrapper around the OLE DB technology.
VFP will automatically convert the ADO RecordSet
into a VFP cursor for us, and will also handle the
updating, just as in the previous examples.
The first thing to do, of course, is to create a new
CursorAdapter class. This time, lets build one
through code.
Start by creating a new program called caADO.prg,
and add the following code:
PUBLIC goCAADO as CursorAdapter

Here, the code populates the cUpdateInsertCmd


parameter, in effect overriding the default Update
command. I use the Evaluate function so the
cursor name will be dynamic, assuming that the
cursor name could easily be changed but the code
may not. Also, I use the OLDVAL function to
retrieve the value the ContactName field had
before it was modified. This is critical to the procedure call as it expects the old value in the Where
clause, much like the automatically generated
Update statement.
Remember that the BeforeUpdate method is
invoked automatically for us by a TableUpdate call
just before the record is actually updated. Therefore,
no matter what the current value is for UpdateCmd,
this method overrides that to always use the stored
procedure.
Note that you could also use the parameterization
discussed earlier, instead of overriding the BeforeUpdate method. This would still require you to
provide the UpdateCmd on the CursorAdapter, but,
instead of hard-coding the parameters you would
use variables or properties and precede them with
question marks.

30 code-focus

An important point to make here is that the


cUpdateInsertCmd (or the objects UpdateCmd)
cannot return a value. More accurately, if you
return a value from the stored procedure, it doesnt
have anywhere to go, and the value is always lost.
Therefore, it is critical that you add the appropriate
RaisError calls in the stored procedure to have your
code respond to any errors that may occur during
the update (such as bad parameters or an update
conflict). You would catch the error by testing the
return value of TableUpdate, calling AError, and
then analyzing the error array.

goCAADO = CREATEOBJECT(caADO)
BROWSE
DEFINE CLASS caADO AS CursorAdapter
oConn = NULL
oRS = NULL
Alias = "cCustADO"
DataSourceType = "ADO"
SelectCmd = "SELECT " + ;
"CustomerID, CompanyName, ContactName, "+;
"ContactTitle, Address, City, Country "+;
"FROM Customers WHERE Customerid LIKE C%"
FUNCTION Init()
This.DataSource = this.oRS
This.oRS.ActiveConnection = this.oConn
This.CursorFill()
ENDFUNC
ENDDEFINE

In this code, we set the DataSourceType to ADO


and add our usual Customers query to the Se-

code-focus

31

Listing 3: The oConn_Access() and oRS_Access() methods

Updating with OLE DB

FUNCTION oConn_Access() as ADODB.Connection


LOCAL loConn as ADODB.Connection
IF VARTYPE(this.oConn)<>"O" THEN
this.oConn = NULL
loConn = NEWOBJECT("ADODB.Connection")
IF VARTYPE(loConn)="O" THEN
loConn.ConnectionString = "Provider=SQLOLEDB.1;Integrated"+;
"Security=SSPI;Persist Security Info=False;Initial "+;
"Catalog=Northwind;Data Source=(local)"
loConn.OPEN()
this.oConn = loConn
ENDIF
ENDIF
RETURN this.oConn
ENDFUNC

Without setting a few more properties, this simple


CursorAdapter is not updatable. The following additional code, inserted in the class definition before
the Init method, will allow automatic updates to
occur:

FUNCTION oRS_Access() as ADODB.RecordSet


LOCAL loRS as ADODB.RecordSet
IF VARTYPE(this.oRS)<>"O" THEN
this.oRS = NULL
loRS = NEWOBJECT("ADODB.Recordset")
IF VARTYPE(loRS)="O" THEN
this.oRS = loRS
ENDIF
ENDIF
RETURN this.oRS
ENDFUNC

lectCmd. When the DataSourceType is ADO, then


the DataSource property must contain either a valid
RecordSet or Command object, depending upon
how you want to use the CursorAdapter. If you
dont parameterize your query (like the earlier examples through use of the ? character) then you
can use a RecordSet; otherwise, you are forced to
use the Command object, simply because thats
where ADO has placed the parameters collection.
Any parameters in your query are automatically
handled by objects in the parameters collection of
the command object.
In this case, well use the RecordSet object, but
notice that we must also provide a Connection
object. In both cases, I am taking advantage of
Access methods to create the references to these
objects. Listing 3 shows the code for the Access
methods.
Both Access methods first check to see if the object
has already been created. If not, then they proceed
with the object creation. In the case of the
RecordSet, you need only to create the object, as
the CursorAdapter takes care of the rest. With the
Connection object, you must provide the connection string and open the connection, because the
CursorAdapter does not open the connection for
you. This is because the connection isnt a property
of the CursorAdapter, but instead is a property of
the RecordSet object.
With this code in place, you can run the program
and see the resultant cursor. It should look very
much like the cursor you retrieved using ODBC,
since it contains the data from the same source.

32 code-focus

KeyFieldList = "CustomerID"
UpdatableFieldList = ;
"CompanyName, ContactName, ContactTitle, "+ ;
"Address, City, Country"
UpdateNameList = ;
"CustomerID Customers.CustomerID, " + ;
"CompanyName Customers.CompanyName, " + ;
"ContactName Customers.ContactName, "+;
"ContactTitle Customers.ContactTitle, " + ;
"Address Customers.Address, "+;
"City Customers.City, Country Customers.Country"
Tables = "Customers"

However, the RecordSet will be created with its


default CursorLocation and CursorType properties.
Without changing these properties, the RecordSet is
initially read-only; therefore, you will need to
modify the oRS_Access method as follows:
FUNCTION oRS_Access() as ADODB.RecordSet
LOCAL loRS as ADODB.RecordSet
IF VARTYPE(this.oRS)<>"O" THEN
this.oRS = NULL
loRS = NEWOBJECT("ADODB.Recordset")
IF VARTYPE(loRS)="O" THEN
loRS.CursorType= 3 && adOpenStatic
loRS.CursorLocation = 3 && adUseClient
loRS.LockType= 3 && adLockOptimistic
this.oRS = loRS
ENDIF
ENDIF
RETURN this.oRS
ENDFUNC

With these additional settings for the RecordSet, the


CursorAdapter can now handle automatic updates.

CursorAdapter with XML


Last, but not least, lets build a CursorAdapter that
uses XML as its data source. This scenario is interesting, since an XML document doesnt normally
act as a data source. Also, the CursorAdapter does
not automatically build SQL Update, Insert or
Delete statements when the data source is set to
XML. Therefore, this type of CursorAdapter will
require the most coding to retrieve and update data.
In this example, I will use the SQLXML feature of
SQL Server 2000 to provide an XML document.
Also, since SQLXML supports updating via XML,
well take the time to write the necessary code to
perform updates. This assumes that you have
configured SQLXML to allow HTTP data access
to the Northwind database, and that you are

allowing updates to the database with UpdateGrams.


In my case, I have set up IIS to use a virtual directory called nwind for HTTP access. Therefore, all
of my examples will contain URLs that reference
http://localhost/nwind

to access SQLXML via IIS.


Lets start by creating a new program called
caXML.prg with the following basic class definition:
PUBLIC oCAXML as CursorAdapter
SET MULTILOCKS ON && need for table buffering
oCAXML = CREATEOBJECT(xcXML)
BROWSE NOWAIT
DEFINE CLASS xcXML AS CursorAdapter
DataSourceType = "XML"
Alias = "xmlCursor"
UpdateCmdDataSourceType = "XML"
InsertCmdDataSourceType = "XML"
DeleteCmdDataSourceType = "XML"
BufferModeOverride = 5
*custom properties
oXMLHTTP = NULL
oXMLDOM = NULL
cServer = "localhost"
cVDir = "nwind"
ENDDEFINE

Listing 4: The GetXML() Method

Beyond the common DataSourceType and Alias


property settings, this is the first time weve seen the
xxxCmdDataSourceType properties. Since this is an
XML-based CursorAdapter, these properties are not
optional if you want it to be updatable. The custom
properties oXMLHTTP and oXMLDOM become
object references used throughout the class, and will
be detailed below.

Retrieving XML Data


Before thinking about the updatability of the
CursorAdapter, lets concentrate on retrieving a
document from the SQLXML server. First, since a
simple Select command will not work, we have to
establish a custom SelectCmd. This is easily done in
the Init method, where we will also invoke the
CursorFill method, as follows:
FUNCTION INIT() as Boolean
LOCAL llRetVal, lcMsg, laErr[1]
this.SelectCmd = "this.GetXml()"
llRetVal = THIS.CursorFill()
IF NOT llRetVal THEN
AERROR(laErr)
lcMsg = "Cursor was not filled!"
IF NOT EMPTY(laErr[2]) THEN
lcMsg = lcMsg + CHR(13) + laErr[2]
ENDIF
MESSAGEBOX(lcMsg,16,"XMLCursorAdapter Test")
ENDIF
RETURN llRetVal
ENDFUNC

FUNCTION GetXml() as String


LOCAL loXMLHTTP as MSXML2.XMLHTTP
loXMLHTTP = this.oXMLHTTP && access method
lcQuery = "SELECT Customerid, Companyname, Contactname, "+;
"Contacttitle, Address, City, Country "+;
"FROM Customers WHERE Companyname LIKE C%25"
lcURL = "http://" + this.cServer + "/" + this.cVDir + "?sql="+;
lcQuery + " FOR XML AUTO, ELEMENTS&ROOT=results"
loXMLHTTP.Open(GET,lcURL,.F.) && do a synchronous GET
loXMLHTTP.Send() && send it
lcRetXML = loXMLHTTP.ResponseText
RETURN lcRetXML
ENDFUNC
FUNCTION oXMLHTTP_Access() as MSXML2.XMLHTTP
LOCAL loXMLHTTP as MSXML2.XMLHTTP
IF VARTYPE(this.oXMLHTTP)<> "O" THEN
this.oXMLHTTP = NULL
loXMLHTTP = NEWOBJECT("MSXML2.XMLHTTP")
IF VARTYPE(loXMLHTTP) = "O" THEN
this.oXMLHTTP = loXMLHTTP
ENDIF
ENDIF
RETURN this.oXMLHTTP
ENDFUNC

34 code-focus

This code establishes the SelectCmd as a local


method instead of a SQL Select command. While
this hasnt been done in the previous examples, this
is perfectly legal for any CursorAdapter class,
regardless of the type. However, when you use a
local method as the SelectCmd, you will have to
also provide custom code for your Update, Insert
and Delete commands, since VFP wont be able to
automatically handle something that is not a SQL
Select command.
When we invoike CursorFill in the Init(), the
GetXML method is called. With the data source set
to XML, the GetXML method must return a valid
XML document that contains only a single table. If
it contains multiple tables, you will get unexpected
results. The GetXML method is shown in Listing 4.
GetXML starts by getting a reference to an
MSXML2.XMLHTTP COM object. This object
handles all of the communication across HTTP,
including sending the queries to the server and
retrieving the results. As you can see, the instantiation
of the oXMLHTTP object is controlled via the
provided Access method, designed to prevent the
constant creation and destruction of this COM server.

Next, you can see our typical Select statement,


except that the LIKE clause is a little different.
HTTP requires that we escape the percent sign
with the hex value of the character, forcing us to
expand it to %25. This value will be collapsed to
the single percent sign character before SQL Server
receives the query.
After that, the code sets up the URL with the
specified query and sends the URL to SQL Server
via HTTP. SQL Server receives the query,
processes it, and returns the result as XML
because weve included the FOR XML clause on
the query. The root element of the XML document is named results in this example. As you
can see from the query string, this is configurable
to your liking.
At this point, lcRetXML contains an XML stream
from the SQL Server. Since the GetXML method
was invoked by VFP as the SelectCmd, you can
simply return the contents of this variable from
the GetXML method and VFP will convert the
stream into a VFP cursor. You can test this by
executing the caXML program. A browse window
should appear with the contents of the returned
XML document. Feel free to use the debugger to
step through the GetXML method so you can see
the XML document in the lcRetXML variable
before it is converted to a VFP cursor and
discarded.

Updating XML Data


The next step is to determine how to make this
cursor updatable so that the changes can be posted
back to our SQLXML server. SQLXML can take a
special XML document, known as an UpdateGram,
and use it to post changes to the database directly.
In VFP7, this document could be created by calling
the XMLUpdateGram function. With VFP 8 and
the CursorAdapter, this is automatically built in
with the UpdateGram property.
The first step is to set up the updatable properties
and establish an Update command. Set up the properties at the top of the class definition and provide
the method call for the Update command by adding
a line of code to the Init method of the
CursorAdapter.
KeyFieldList = customerid
Tables = customers
UpdatableFieldList = ;
"companyname, contactname, contacttitle, "+;
"address, city, country "
UpdateNameList= ;
"customerid customers.customerid, " + ;
"companyname customers.companyname, " + ;
"contactname customers.contactname, " + ;
"contacttitle customers.contacttitle, " + ;
"address customers.address, " + ;
"city customers.city, country customers.country"

Listing 5: The UpdateXML() method and oXMLDOM_Access() method


PROCEDURE UpdateXML()
LOCAL loXMLHTTP as MSXML2.XMLHTTP, loXML as MSXML2.DOMDocument, ;
loNode as MSXML2.IXMLDOMNode, lcRetVal, lcErrMsg, ;
lcAttrib, lnStart, lnEnd
loXMLHTTP = this.oXMLHTTP
loXML = this.oXMLDOM
IF loXML.loadXML(This.UpdateGram) THEN
loXMLHTTP.Open("POST", "http://" + this.cServer + ;
"/" + this.cVDir, .F. )
loXMLHTTP.setRequestHeader("Content-type", "application/xml")
loXMLHTTP.send(loXML)
lcRetVal = loXMLHTTP.responseText
loXML.loadXML(lcRetVal)
loNode = loXML.selectSingleNode("root/pi(MSSQLError)")
IF VARTYPE(loNode)="O" THEN
*--we have an error from MSSQL
lcErrMsg = loNode.Text
lcAttrib = Description="
lnStart = AT(lcAttrib,lcErrMsg) + LEN(lcAttrib)
lnEnd = RAT(", lcErrMsg)
lcErrMsg = SUBSTR(lcErrMsg, lnStart, lnEnd - lnStart)
ERROR (lcErrMsg) && generate an error so TableUpdate fails
ENDIF
ENDIF
ENDPROC
FUNCTION oXMLDOM_Access() as MSXML2.DOMDocument
LOCAL loXMLDOM as MSXML2.DOMDocument
IF VARTYPE(this.oXMLDOM)<> "O" THEN
this.oXMLDOM = NULL
loXMLDOM = NEWOBJECT("MSXML2.DOMDocument")
IF VARTYPE(loXMLDOM) = "O" THEN
this.oXMLDOM = loXMLDOM
ENDIF
ENDIF
RETURN this.oXMLDOM
ENDFUNC

FUNCTION INIT() as Boolean


LOCAL llRetVal, lcMsg, laErr[1]
this.UpdateCmd = "this.UpdateXML()"
this.SelectCmd = "this.GetXML()"
** balance of code skipped

Note that we could have placed the property


settings for UpdateCmd and SelectCmd in the list of
properties that precede the Init method it works
the same either way. Regardless, the first part of this
code should be familiar by now, where we set the
KeyFieldList, Tables, UpdatableFieldList and
UpdateNameList properties. Without these property
settings, no UpdateGram can be created.
After that, we establish the UpdateXML method as
the CursorAdapters UpdateCmd. There are no parameters passed to the UpdateXML method, however,
so all the work of determining the changes must be
handled within this method. Also, since an XMLtype CursorAdapter has no default update mecha-

code-focus

35

Listing 6: The UpdateGram created by changing the ContactName on Customer CACTU


<?xml version = "1.0" encoding="Windows-1252" standalone="yes"?>
<root xmlns:updg="urn:schemas-microsoft-com:xml-updategram">
<updg:sync>
<updg:before>
<customers>
<customerid>CACTU</customerid>
<contactname>Patricio Simpson</contactname>
</customers>
</updg:before>
<updg:after>
<customers>
<contactname>New Value</contactname>
</customers>
</updg:after>
</updg:sync>
</root>

nism, you must write the code to post the changes to


the XML data source. This is all done in the code for
UpdateXML (and oXMLDOM_Access), shown in
Listing 5.
In this code, we use the XMLHTTP object to post
the changes to the server. This is done by loading
the contents of the UpdateGram property into an
XMLDOM (instantiated by the included Access
method) with the LoadXML method, opening a
connection to the server, setting the content of the
request as XML, and then sending the XMLDOM.
Any result is returned via the XMLHTTP objects
ResponseText property, which is subsequently
loaded in the XMLDOM and analyzed for any error
messages.
If no errors are detected, the update has succeeded
and the procedure ends. However, if there are
errors, the text of the error message is parsed and
included in a user-defined Error so the TableUpdate
function can see the failure. Without this code, your
TableUpdate call would always return success, even
though there might be a problem.
To test this code, execute the caXML program,
make a change to one of the fields in the cursor,
and then issue TableUpdate from the command
window. If TableUpdate succeeds, you should be
able to see your change on the server. However, if
TableUpdate fails, you will need to use the AError
function to retrieve the error message generated by
SQL Server.
If you are curious about the contents of an UpdateGram, you can step through the UpdateXML
method of the class and check the contents of the
UpdateGram property once you are inside this
method. However, if youre not in one of the data
modification methods (as specified in the
UpdateCmd, InsertCmd, or DeleteCmd properties),
you cannot see the contents of the UpdateGram
property.
Listing 6 shows the contents of an UpdateGram
when the ContactName field has been changed on
the Customer record with the ID of CACTU.

36 code-focus

As you can see, SQLXML can read this document


and easily build an Update-SQL statement, which it
then posts to the SQL Server. The updg:sync
element is closely related to starting a transaction;
therefore, if you have multiple tables to update, you
could combine them into a single UpdateGram,
ensuring they are encapsulated within this element,
and they will be wrapped within a transaction.

Final Thoughts
In this article, weve covered a lot of ground,
showing the four faces of the new CursorAdapter
class. Youve seen how to build the CursorAdapter
through the DataEnvironment and CursorAdapter
builders, through the visual class designer, and
through a PRG. Youve also seen the basics of
building CursorAdapter classes for native, ODBC,
OLE DB or XML data access, and how to make
each one of these classes updatable as well.
The next step is to think about how to apply these
classes to your everyday development efforts. In my
opinion, I can see the CursorAdapter class working
very well in the UI layer of any application, and also
in certain kinds of business objects where there is
lots of processing code to implement. The
CursorAdapter, as noted earlier, is not a good
choice of object for passing data between tiers, as it
converts everything into a non-portable VFP cursor.
However, in a scenario where a business object uses
a CursorAdapter class, it can receive the data from
the data source, and then process that data using
standard VFP commands and functions, since it is
in a VFP cursor. When finished, that data could be
converted to a more suitable type for cross-tier
marshalling, such as XML.
The other advantage of the CursorAdapter is the
common OOP interface, regardless of the type of
data that it accesses. Even with the XML version,
which requires the most coding to make updatable,
we still retrieved the data using CursorFill, updated
data with TableUpdate, and retrieved errors with
AError, as with every other type of CursorAdapter.
With a little forethought and planning, you could
conceivably build a reusable set of classes, based
upon the CursorAdapter, that could then be
tweaked for each individual data source. These
classes could be reused between applications or
mixed within the same application to standardize
the way your application handles data.

Chuck Urwiler

Structured Error
Handling in VFP 8
Markus Egger
megger@eps-software.com

Markus Egger is president of


EPS Software Corporation,
located in Houston, Texas. He
is also the founder of EPS Software Austria, located in
Salzburg.
He concentrates on consulting
in COM-based, object-oriented
development and Internet applications. He is an international
author and speaker, and is copublisher of Component Developer Magazine. He is also the
author of Advanced ObjectOriented Programming with
Visual FoxPro, from Hentzenwerke Publishing.
For the past several years,
Markus has received the
Microsoft MVP award.
Several applications he has
worked on (mostly as project
manager) have received
Microsoft Excellence Award
nominations.
He is the author of several
tools, such as GenRepoX
(public domain), the Fox Extension Classes, and Visual
WebBuilder.
A full bio can be found on the
web at: www.epssoftware.com/MarkusEgger

With the introduction of Visual FoxPro 3.0, error handling in VFP


changed substantially. Rather than using on error statements, state of the
art error events became available. Now, 7 years later, more sophisticated error
handling mechanisms take center stage as Visual FoxPro 8.0 introduces structured
error handling.
andling potential errors in
the most graceful way has
been a goal of software
developers since the very early
days of programming, and the
quest for the perfect methodology
is still ongoing. FoxPro and
Visual FoxPro have gone through
a number of different ways to
handle errors (all of which are
still available today and are useful
for different scenarios).

Visual FoxPro 8.0 introduces a


new error handling mechanism
known as Structured Error
Handling. This mechanism uses
Try/Catch blocks to wrap sections of source code and attach
the appropriate error handler.

The most traditional way to handle errors in


FoxPro (and even in FoxBase, before) was the ON
ERROR statement. This command tells FoxPro
what to do in case of an error. Arguably one of the
most common scenarios would be to call a procedure that handles an error in the following fashion:

Unfortunately, this doesnt work,


because whatever is specified as
the ON ERROR statement will
run as if it were a separate
procedure. In other words, this
line of code will run outside the
object. Therefore, the THIS
pointer is not valid.
Another issue is that the ON
ERROR statement would not be
scoped to the object. Consider
the following example:

ON ERROR * && Ignore errors


LOCAL loExample
loExample = CREATEOBJECT("Example")
xxxxxxx
&& Syntax error
RETURN

ON ERROR DO ErrorHandler

Or, you might use a slightly more sophisticated


version, as suggested by the Visual FoxPro documentation:
ON ERROR DO errhand WITH ;
ERROR( ), MESSAGE( ), MESSAGE(1),;
PROGRAM( ), LINENO( )

Of course, in the object-oriented world that Visual


FoxPro lives in, this is a very procedural way to
handle things. Luckily, the ON ERROR command
can evaluate any Visual FoxPro expression,
including calling methods on an object:
ON ERROR oErrorHandler.Handle()

This approach works rather well in scenarios where


a global error handler is used. However, this type of
error handler is generally not used in an objectoriented environment. There are a number of
reasons for this. First of all, in order to create blackbox objects, those objects have to handle their own
errors to conform to the rules of object-oriented
development. However, to make those objects
handle their own errors, we would have to set up an
error handler like so:
ON ERROR THIS.HandleErrors()

38 code-focus

Fast Facts

DEFINE CLASS Example AS Custom


FUNCTION Init
ON ERROR THIS.HandleErrors
RETURN .T.
ENDFUNC
FUNCTION HandleErrors
MESSAGEBOX("Handling Errors...")
ENDFUNC
ENDDEFINE

In this particular example, the first line instructs VFP


to ignore all errors (the error statement is an asterisk,
which is a comment line). Then, the Example object
gets instantiated, and its constructor (Init()), sets the
error statement to THIS.HandleErrors (for now,
lets just assume that would be a valid line of code).
After the object is instantiated, a line of code with a
syntax error (xxxxxxx) executes, raising an error.
The question is: What error handler will handle that
error? Since we now know that ON ERROR is not
scoped to objects, we also know that the error is
handled by THIS.HandleErrors. Clearly, even if
that would call a method on the right object, this
wouldnt be desired, since the object has no knowledge about how to handle errors that may occur
outside the object. Similarly, error handlers defined

after the object is instantiated would throw off error


handling within the object. Neither scenario will
allow us to create black box objects.

Each Try-block needs to have


at least a CATCH
or a FINALLY block.

One possible solution would be to create an object


devoted to error handling. This object could be
created when the main object gets instantiated.
However, to make this work, the new object would
have to be referenced through a public variable so it
could be referenced (again, THIS.oError.HandleErrors() would not work). This could lead to collisions
with other objects that employ the same approach.
Also, each individual method would have to set the
error handler to that handler object, and reset it
back not only when the method completed, but also
every time the object called out to other code
(which may or may not use a similar approach).
This certainly would be an error-prone solution.
Lets not even investigate it any more, although I
could point out a long list of other problems.
Clearly, a better way to handle errors was required.
For this reason, Visual FoxPro 3.0 (the first Visual

version of FoxPro and also the first version that


supported object-oriented development) introduced
an Error() event. Using that mechanism, errors
could be handled in the following fashion:
ON ERROR * && Ignore errors
LOCAL loExample
loExample = CREATEOBJECT("Example2")
xxxxxxx
&& Syntax error
RETURN
DEFINE CLASS Example2 AS CUSTOM
PROCEDURE INIT
xxxxx
&& Syntax error
ENDPROC
PROCEDURE ERROR(nError, cMethod, nLine)
MESSAGEBOX("Error inside the object")
ENDPROC
ENDDEFINE

The idea here is simple: Whenever an error occurs


anywhere within the object, the Error() event will
fire. If the error occurred anywhere outside the
object, it will be handled by whatever error handler
is defined there. In our example, the syntax error in
the Init() method will be handled by the Error()
method, and the syntax error in the line of code
after the CreateObject() will be handled by the ON
ERROR error handler (which will actually hide the
error).

code-focus

39

This mechanism has a number of advantages. First


of all, it allows building self-contained objects.
Secondly, it splits the gigantic task of handling
errors globally, into smaller, more digestible pieces.
No longer are we dealing with handling a very large
number of errors. For instance, if the object at hand
doesnt deal with database tables, we probably dont
have to worry about handling any database errors.
However, this approach also has some problems.
For example, it still may be handling errors on a
scale much larger than we want. Objects can be
large and do a large number of different things, each
of which may have only a very limited number of
scenarios that may go wrong. In total, however, the
object might require a very complex error handler.
Another problem is that this type of error handler
makes it very difficult to exit gracefully whenever an
error has occurred. Consider the following example:
DEFINE CLASS WordExport AS Custom
FUNCTION Export(lcText1,lcText2)
LOCAL oWord as Word.Application
oWord = CREATEOBJECT("Word.Application")
oWord.Application.Visible = .T.
oWord.Documents.Add()
oWord.Selection.InsertAfter(lcText1)
oWord.Selection.InsertAfter(lcText2)
RETURN .T.
ENDFUNC
PROCEDURE ERROR(nError, cMethod, nLine)
MESSAGEBOX("Error exporting to Word!")
ENDPROC
ENDDEFINE

The idea behind this simplified example is that the


WordExport object can be used to create a Word
document on the fly. To do so, the developer simply
instantiates this object and passes some text to the
Export() method. The method then opens an
instance of Word, makes it visible, creates a new
document and exports the text.
What would happen if the user actually closed
Word right after a new document has been created
(right after the Documents.Add() line)? Well, the
next two lines of code would both cause an error
(and so would hundreds of other lines if this was a
life-size example).
But what could our error handler do to solve the
problem? Well, beyond displaying the error in a
message box, the error handler could try to fix the
problem. However, this is unlikely in this case,
because in order to do that, the method would have
to start over from scratch. Since that isnt something
the error handler could do easily, it can choose to
ignore the error and proceed with the next line of
code, which would then cause another error that
could also be ignored, and so forth.
Another option would be to issue a RETRY, which
would run the line that failed again, causing another

40 code-focus

error, which would result in an endless loop if the


handler just tried to RETRY again. The only other
option we have would be to CANCEL, which
would shut down the whole process and not just
the current method.
Note also, that the method returns .T., which is the
way I would like things to be if the document got
created successfully. However, I would like the
method to return .F. if there was a problem. This
isnt so easy, since the Error() event doesnt have
any access to the return value of this method.
One possible solution would be a local ON ERROR
statement instead of the error method:
DEFINE CLASS WordExport AS Custom
FUNCTION Export(lcText1,lcText2)
* We create a new error handler
LOCAL lError
lError = .F.
ON ERROR lError = .T.
* We run the regular code
LOCAL oWord as Word.Application
oWord = CREATEOBJECT("Word.Application")
oWord.Application.Visible = .T.
oWord.Documents.Add()
oWord.Selection.InsertAfter(lcText1)
oWord.Selection.InsertAfter(lcText2)
* We reset the error handler,
* and check if everything went fine
ON ERROR
IF lError
RETURN .F.
ELSE
RETURN .T.
ENDIF
ENDFUNC
ENDDEFINE

This is an acceptable solution, but there are difficulties with this approach. First of all, the method
might call out to other methods that may reset the
error handler or point to a different handler. This is
a problem that is hard to avoid, since you may not
have control over other code that is running.
Also, at a later point in time, someone may want to
add an Error() method to this object (perhaps to
handle errors that may occur in other methods).
The problem with that is that the error method
takes precedence over the ON ERROR handler,
hence rendering the ON ERROR useless.

Introducing: Try/Catch
To solve these issues, Visual FoxPro 8.0 introduces
Structured Error Handling. This approach allows
the developer to wrap a series of commands into a
block that is handled by a local error handler. The
advantage of this error handler is that it usually

handles a very limited set of potential problems,


making it simple and straightforward.
This is the basic syntax for structured error handling
in Visual FoxPro:
TRY
* Do something
CATCH
* Handle a potential problem
ENDTRY

Lets see how we could re-work the above example


into a scenario handled by a Try/Catch block:
DEFINE CLASS WordExport AS Custom
FUNCTION Export(lcText1,lcText2)
LOCAL lReturnValue
lReturnValue = .T.

Note that the Catch-block is never executed if no


error occurs. Sometimes you might want to define
code that runs as cleanup code, whether an error
occurred or not. Here is an example:
DEFINE CLASS WordExport AS Custom
FUNCTION Export(lcText1,lcText2)
LOCAL lReturnValue
lReturnValue = .T.
TRY
* We run the regular code
LOCAL oWord as Word.Application
oWord = CREATEOBJECT("Word.Application")
oWord.Application.Visible = .T.
oWord.Documents.Add()
oWord.Selection.InsertAfter(lcText1)
oWord.Selection.InsertAfter(lcText2)
CATCH
lReturnValue = .F.

TRY

CATCH
lReturnValue = .F.
ENDTRY
RETURN lReturnValue
ENDFUNC
ENDDEFINE

As we can see, this is a much simpler way to implement the solution. First of all, it is simply much less
kludgy and is a very clean implementation. But
more importantly, it is a much superior implementation from a technical point of view. The solution
is not influenced by outside error handling.
Also, we have full control over what is to happen
if an error does occur. Unlike in the example with
the error event, we can write code within our
method that executes no matter whether an error
occurred or not, making it easy to set the return
value to our liking. (We were able to do this in the
previous example, but the solution was error prone
and easy to break by running it in different environments).
Im sure that by now you already have a good idea
about what Try/Catch does: Whatever code we run
inside a Try-block will execute until an error
occurs. If an error does in fact occur, the Catchblock is executed. Note that the try block stops
executing as soon as an error occurs. There is no
way to retry or ignore the error. If thats what you
would like to do, Try/Catch error handling is not
the right solution.

FINALLY
IF VarType(oWord) = "O"
oWord.Application.Quit()
ENDIF
ENDTRY
RETURN lReturnValue
ENDFUNC
ENDDEFINE

In this example, we shut down Word, even if something went wrong. Note however, that the error may
have occurred before Word ever got instantiated.
Therefore we need to first check whether Word is
an object. (Actually, things may be a little trickier
with automation objects, especially Word, but for
simplicity well leave it at that.)
At this point you may wonder why we need a
Finally-block. After all, we could have put that code
after the ENDTRY and would have achieved an
identical result. However, there are scenarios that
can greatly benefit from using the finally-block
(which we will examine further down), making the
use of FINALLY a good idea in general.

Esther Fan
User Education Writer,
Visual FoxPro Team
Microsoft
There are many great new
features in Visual FoxPro 8.0!
Possibly the most popular of
them all is the new TRY/CATCH
structured error handling,
similar to various .NET and
other programming languages.
In addition to new features in
VFP 8.0, we made many documentation improvements.
Community feedback is important, so each VFP 8.0 Help topic
now includes an e-mail hyperlink labeled, "Send feedback on
this topic to Microsoft." You
can now send your comments
and suggestions about the VFP
8.0 documentation to us
directly so we can make even
more improvements in the
future.

One last remark about the basic Try/Catch structure:


Each Try-block needs to have at least a CATCH or a
FINALLY block. Therefore, you can not just say try
this, and I dont care of it works or not since I cant
do anything about a potential problem anyway. If
you would like to do that, you can create a Catchblock that has nothing but a comment. A scenario
like this may be desired within an error handler:

Try/Catch blocks can be nested


to achieve more granular error
handling.

* We run the regular code


LOCAL oWord as Word.Application
oWord = CREATEOBJECT("Word.Application")
oWord.Application.Visible = .T.
oWord.Documents.Add()
oWord.Selection.InsertAfter(lcText1)
oWord.Selection.InsertAfter(lcText2)

From the VFP Team

code-focus

41

Try/Catch in .NET
Visual Studio .NET uses a very
similar mechanism as Visual
FoxPro 8.0. As one would
expect, due to .NETs integral
thread-safety, the mechanism
there is a little more structured,
but also a little less flexible. For
instance, VFP8 can throw any
variable or object as a custom
error. In .NET, only exception
objects can be thrown. On the
other hand, .NET doesnt differentiate between user errors and
system errors, which has
advantages as well.

TRY
USE Customer
LOCATE FOR LastName = "Gates"
IF FOUND()
StrToFile("Gates found!","customer.log")
ENDIF
CATCH
TRY
StrToFile("Error: "+Message(),"Error.log")
CATCH
* Nothing we can do now
ENDTRY
FINALLY
IF Used("Customer")
USE IN Customer
ENDIF
ENDTRY

This example also demonstrates one of the key


features of structured error handling: Nested
Try/Catch blocks.

Nested Try/Catch Blocks


Try/Catch blocks can be nested to achieve more
granular error handling. There may be a Try/Catch
block around the entire application, there may be
Try/Catch blocks wrapping entire methods, then
there may be individual blocks, and so forth.
Lets enhance our Word example a little more and
instead of creating a blank document, we will create
a new one based on a certain template:
FUNCTION Export(lcText1,lcText2)
LOCAL lReturnValue
lReturnValue = .T.
TRY
* We run the regular code
LOCAL oWord as Word.Application
oWord = CREATEOBJECT("Word.Application")
oWord.Application.Visible = .T.
TRY
oWord.Documents.Add("MyTemplate.dot")
CATCH
oWord.Documents.Add()
ENDTRY
oWord.Selection.InsertAfter(lcText1)
oWord.Selection.InsertAfter(lcText2)
CATCH
lReturnValue = .F.
ENDTRY
RETURN lReturnValue
ENDFUNC

In this example, the inner Try/Catch block traps


only errors that may occur while a new document is
created based on the specified template. Presumably, if that template doesnt exist, an error will be
raised and caught by the Catch-block, which will
create a blank document. The code then proceeds
as planned.

42 code-focus

Note that the Catch-block may raise another error


that will then be handled by the outer Catch-block
(which simply sets the return value and gives up).
There is one potential problem here. We are assuming
that the error has been caused by the fact that the
template doesnt exist. But of course, there could be a
number of other scenarios causing other problems.
For instance, the problem could be caused by the user
closing Word right after it became visible (yes, theyd
have to be very quick, but hey, this is only an
example!). In our little example, this wouldnt be a
problem. Worst case, the Catch-block fails again and
defaults to the outer handler, which will handle the
situation appropriately. However, in many complex
scenarios, we would have to look at additional error
information and handle the situation appropriately.

Conditional Error Handling


Visual FoxPro has a number of functions to retrieve
error information, such as Message(). However,
those functions are not really adequate to make this
bullet-proof, since nested errors make things a bit
complicated. For this reason, Microsoft introduced
an Exception object. The exception object can be
invoked simply by using it on the CATCH statement:
CATCH TO oException

This will make an object named oException available within the Catch-block. This object has a
number of properties, such as ErrorNo, Message,
LineNo, Details, LineContents, and more. Using this
construct, we can use the following syntax to check
for errors caused by the template only:
FUNCTION Export(lcText1,lcText2)
LOCAL lReturnValue
lReturnValue = .T.
TRY
* We run the regular code
LOCAL oWord as Word.Application
oWord = CREATEOBJECT("Word.Application")
oWord.Application.Visible = .T.
TRY
oWord.Documents.Add("MyTemplate.dot")
CATCH TO oException
IF oException.ErrorNo = 1429
oWord.Documents.Add()
ELSE
* We have a different problem
THROW oException
ENDIF
ENDTRY
oWord.Selection.InsertAfter(lcText1)
oWord.Selection.InsertAfter(lcText2)
CATCH
lReturnValue = .F.
ENDTRY
RETURN lReturnValue
ENDFUNC

This is a pretty simple example. All we really check


for is the error number. But, imagine we check for
other conditions. For instance, we could try to find
another template, or download it from somewhere,
and so forth. If all of those attempts fail, we would
re-throw the error. If all we wanted to check was
the error number, though, we could do something
even simpler:
TRY
oWord.Documents.Add("MyTemplate.dot")
CATCH TO oEx WHEN oEx.ErrorNo = 1429
oWord.Documents.Add()
ENDTRY

The WHEN clause of the CATCH


statement can utilize any valid
Visual FoxPro expression.

In this example, we handle only error 1429, which


is the one that is raised if the template wasnt there.
The question is: What do we do with all other
errors? Well, basically, we want it to be handled the
same way all other errors are handled within the
outer Try-block. Therefore, we need to elevate the
error to that level. We can do so using the THROW
statement. This will re-throw the error, causing it
to be handled by the outer Catch-block. (Exceptions
elevated using a THROW statement will end up as
user exceptions in the outer error handler. See
below for more information.)

In this scenario, only the first catch-block will ever


be executed, because it is so generic, it will catch all
the errors and the subsequent catch statements will
never be evaluated.
The WHEN clause of the CATCH statement can
utilize any valid Visual FoxPro expression. Note,
however, that to avoid having an erroneous catch
statement you shouldnt make these statements too
complex.

Throwing Custom Errors


As we have seen in previous examples, the new
THROW command can be used to elevate errors
the error handler chooses not to handle, so an outer
error handler (perhaps another Catch-block, or
some other type of error handler) can attempt to

This will catch only error 1429. All other errors will
be automatically elevated to the outer error handler,
if there is one. Otherwise, the default VFP error
dialog would be shown. Therefore, this is a shortcut
that is functionally identical to the version shown in
the previous example (except that the exception
elevated to the outer handler will not be a user error).
What makes this feature very powerful is that there
can be a number of different catch-blocks:
TRY
oWord.Documents.Add("MyTemplate.dot")
CATCH TO oEx WHEN oEx.ErrorNo = 1429
oWord.Documents.Add("MyOtherTemplate.doc")
CATCH TO oEx WHEN oEx.ErrorNo = 1943
MessageBox("Stop closing Word!!!")
CATCH
MessageBox("Something else happened!")
ENDTRY

Note that catch blocks are evaluated from top to


bottom, and only one of them will run. Therefore,
the chosen sequence is important. If we change this
example to the following, we would see unexpected
(or expected after you read this article) results:
TRY
oWord.Documents.Add("MyTemplate.dot")
CATCH
MessageBox("Something else happened!")
CATCH TO oEx WHEN oEx.ErrorNo = 1429
oWord.Documents.Add("MyOtherTemplate.doc")
CATCH TO oEx WHEN oEx.ErrorNo = 1943
MessageBox("Stop closing Word!!!")
ENDTRY

code-focus

43

handle the error. Whats not as obvious is that


THROW can be used to raise custom errors,
allowing us to architect our applications in an
entirely different fashion.
Listing 1 shows an example for this technique. In
this example, we have a class called CreditCard that
simulates a credit card charging object. This object
is rather simple. All it has is one method called
ChargeCard(), and all that method does is check if
the passed credit card number is 12345678. If so,
the card is considered valid. This is a simplistic
example, but all we are really interested in is the
error handling. So lets see what happens when the
card number is invalid.
Listing 1: Raising a custom exception
TRY
LOCAL loCharger AS CreditCard
loCharger = CREATEOBJECT("CreditCard")
loCharger.ChargeCard("Me","56789012","01/01")
* We look for user-thrown errors...
CATCH TO oEx WHEN oEx.ErrorNo = 2071
IF oEx.UserValue.ErrorNo = 10001
* This is a credit card exception
MESSAGEBOX(oEx.UserValue.ErrorDetail,16,"Alert!")
ELSE
THROW oEx
ENDIF
* We can still use a generic error handler
CATCH
MESSAGEBOX("Different Problem")
ENDTRY
RETURN

DEFINE CLASS CreditCard AS Custom


FUNCTION ChargeCard(lcName, lcNumber, lcExpDate)
* Credit card charging logic goes here
IF NOT lcNumber = "12345678"
* We simulare an invalid card
LOCAL loException AS CreditCardException
loException = CREATEOBJECT("CreditCardException",;
"Invalid Credit Card Number!")
THROW loException
RETURN .F.
ELSE
* Everything is fine
RETURN .T.
ENDIF
ENDFUNC
ENDDEFINE

First of all, the ChargeCard() method instantiates a


class called CreditCardException and passes some
detailed error information to its constructor. This
class is defined a little further down and is a simple
subclass of the new Visual FoxPro Exception base
class. It has a few overridden properties, and one
additional one that gets set based on the value
passed to the constructor. Once that object is
instantiated, the CreditCard class raises an error
(exception) using the THROW command and the
new exception object as the expression. This will
immediately halt the execution of the ChargeCard()
method, and invoke whatever error handler is
currently in use.
So now lets work our way back up towards the
beginning of this listing to see how this code is
invoked. The listing starts out with the instantiation
of the credit card object and a call to the ChargeCard() method. The parameter passed to this
method represents an invalid credit card (error
handling is easier to demonstrate if things fail). All
of this is wrapped into a Try/Catch block.
Note that the Catch-block traps for error 2071. All
user-thrown exceptions end up as error 2071. In
this particular example, those are all the errors we
are really interested in. Of course, there could be
other errors occurring, and those are caught by the
second Catch-block. In a larger example, there
could also be an outer error handler so we wouldnt
have to worry about that possibility. The second
Catch-block is not required and I just included it
because Id consider it good form.
So what exactly happens when a user-thrown error
occurs and our Catch-block kicks in? Well, first of
all, there could be a number of different userthrown errors, and we are not interested in any of
them other than our custom exception. The user
defined information is stored in a property called
UserValue, which is a variant and could be
anything. In our case, it is another exception object,
since thats what we threw, but it could be a string
or any other value if the exception was thrown in
the following manner:
THROW "Something is wrong!"

Since we threw an object, we can now check for


detailed information on that object, such as the
error number or perhaps even the class. If we
discover error number 10001 (which is our custom
error number), we can handle it. Otherwise, it is a
different user-thrown error, and we really do not
know what to do at all, so we simply elevate the
error to the next level by re-throwing it.

DEFINE CLASS CreditCardException AS Exception


Message = "Failed to charge credit card."
ErrorNo = 10001
ErrorDetail = ""

Note that this example is not bullet-proof. The


following line of code may, in fact, cause other errors:

FUNCTION Init(lcDetailedMessage)
THIS.ErrorDetail = lcDetailedMessage
ENDFUNC
ENDDEFINE

If UserValue is not an object, or if it is an object but


doesnt have a property called ErrorNo, this would
result in yet another exception, which would be
thrown to an outer exception handler. Note that the

44 code-focus

IF oEx.UserValue.ErrorNo = 10001

Whats not as obvious is that


THROW can be used to raise
custom errors, allowing us to
architect our applications in an
entirely different fashion.

outer exception handler would receive a FoxPro


error, and not the user thrown error, which would
not be a good thing at all.
At this point, you may wonder how UserValue
could be an object but not have that property. The
reason is simple: Just like one can throw a string or
a number as the user value, one could throw any
type of object as the user value. The thrown object
doesnt have to be subclassed from Exception.
One of the gotchas with this type of architecture
is that youi should really use Try/Catch blocks to
catch these user thrown errors. Technically, you can
use ON ERROR to catch our CreditCardException,
but it is a bit trickier to do so since no error object
is available.

represents some kind of error in these examples).


What would we expect to happen here?
Most people I present this to would expect the ON
ERROR to handle the first problem, and the Catchblock to handle the second error. This is not the
case! The Catch-block takes precedence over the
ON ERROR and handles both exceptions.
At this point, you may wonder why one would ever
define an ON ERROR inside a Try/Catch. In realworld environments, this is a rather common
scenario. Consider this example:
TRY
ErrTest()
CATCH
MESSAGEBOX("Exception!")
ENDTRY
FUNCTION ErrTest
ON ERROR MESSAGEBOX(MESSAGE())
xxxxx
ENDFUNC

The Try/Catch wraps a simple call to another function (or method). That function apparently has its

One last word of caution: The use of a THROW


statement will always end up as a user thrown
error. This means that if you intend to elevate an
error from within a catch block to an outer error
handler, you may be re-throwing a system error, but
it will end up as a user error in the next-level error
handler. The original (system) exception object will
end up as the UserValue. Of course, to handle these
situations correctly, the outer exception handler
needs to be aware of this.

Mixing Error Handling Methodologies


Structured error handling is great and will replace
traditional error handling in most scenarios. In fact,
some modern languages like C# have only structured
error handling. However, there are some downsides
to structured error handling, such as no intrinsic retry
capability. Also, in many scenarios, pre-existing, nonstructured error handling may be in place.
So lets look at a few examples of mixed error
handling and the effects it may have on your code.
Lets start out with a simple one:
TRY
ON ERROR MESSAGEBOX(MESSAGE())
xxxxx
ON ERROR
xxxxx
CATCH
MESSAGEBOX("Exception!")
ENDTRY

In this example, we define an ON ERROR statement within a Try/Catch block (xxxxx always

code-focus

45

own error handling using the old ON ERROR


methodology. However, the local error handling
mechanism used by that function is now taken
hostage by our Catch-block. As you can imagine,
this may result in some surprising behavior.
We can produce a similar example using the Error()
method:
TRY
oTest = CREATEOBJECT("TestClass")
oTest.Execute()
CATCH
MESSAGEBOX("Exception!")
ENDTRY

In this example, the Error() method will get a


chance to handle the re-thrown error. The outer
error handler will not have the opportunity to
handle the exception, because it is not possible to
elevate the error from within the Error() method
because the exception object is not available there.
The only option would be to throw a custom error.

DEFINE CLASS TestClass AS Custom


FUNCTION Execute
xxxxxx
ENDFUNC

Finally

FUNCTION Error(nError, cMethod, nLine)


MESSAGEBOX(MESSAGE())
ENDFUNC
ENDDEFINE

In this example, we are also calling another method


that has a local error handler. However, this time the
result is opposite from the previous example. The
Error() event takes precedence over the Try/Catch
and handles the error inside the called object.
So what would happen if we added some structured
error handling to the TestClass object?
DEFINE CLASS TestClass AS Custom
FUNCTION Execute
TRY
xxxxxx
CATCH
MESSAGEBOX("Exception 2!")
ENDTRY
ENDFUNC
FUNCTION Error(nError, cMethod, nLine)
MESSAGEBOX(MESSAGE())
ENDFUNC
ENDDEFINE

In this example, the new Try/Catch will handle the


error since it has been defined at a higher level of
granularity.
An interesting question here is, What happens if
that Catch-block re-throws the error?
DEFINE CLASS TestClass AS Custom
FUNCTION Execute
TRY
xxxxxx
CATCH TO oEx
MESSAGEBOX("Error!")

46 code-focus

THROW oEx
ENDTRY
ENDFUNC
FUNCTION Error(nError, cMethod, nLine)
MESSAGEBOX(MESSAGE())
ENDFUNC
ENDDEFINE

I still owe you an explanation of the FINALLY


statement. In many scenarios, it may seem as if
FINALLY may not really be required, since the flow
of the program is likely to continue after the
Try/Catch section. Likely is the key term here. If
a potential error is not handled in a Catch-block
(either because there isnt a matching Catch-block
or because another exception is THROWn), code
after the Try/Catch statements may not be executed
at all. Consider this example:
DEFINE CLASS TestClass AS Custom
FUNCTION Execute
TRY
xxxxxx
CATCH TO oEx
MESSAGEBOX("Error!")
THROW oEx
FINALLY
MESSAGEBOX("Cleanup Code")
ENDTRY
MESSAGEBOX("More Code")
ENDFUNC
ENDDEFINE

In this example, the syntax error in the Try-block is


caught by the Catch-block, just to be re-thrown
again. This means that the very last MessageBox()
will never be executed. However, the MessageBox()
in the Finally-block will be executed in every case,
even if no exception occurred.

Conclusion
Structured Error Handling is one of the most
important language enhancements Visual FoxPro
has seen in a while. It is very powerful and helps
you tremendously in your attempts to produce
bullet-proof code.
If you have any questions about this technology, feel
free to email me.
Markus Egger

Member Classes
Bring Flexibility
Garrett Fitzgerald

The new VFP 8 feature often referred to as Member Classes


is a set of new properties and new ways to define classes that
can bring much more flexibility when working with certain
controls. Need to define several pages in a pageframe with different properties
and settings? No problem. How about better control of grid column headers? No
problem.

garrett@donnael.com

Garrett Fitzgerald, of donnael


Consulting, has been working
with various flavors of Fox
since 1994, from FoxBASE to
VFP 7. He received the
Microsoft Most Valuable
Professional award for online
peer support in 1998 and 1999,
and followed that up with a
stint in Microsoft Product
Support Services. Hes
currently working full-time as a
contractor with Volt Information Sciences.

Fast Facts
By changing the way you can define
and work with member classes,
Microsoft has given VFP developers
tremendous flexibility in working
with pageframes and grids.
n Visual FoxPro, there are certain classes that have
meaning only when theyre in containers: for
example, Pages and Headers can live only in PageFrame and Column objects. In versions before VFP 8,
if we wanted to subclass these classes, we needed to
manually add them using the AddObject method.
But, there were problems with this approach. For
example, this kept us from putting controls on a
subclassed page in the Form Designer.

However, VFP8 adds the MemberClass and


MemberClassLibrary properties, as well as the
HeaderClass and HeaderClassLibrary properties for
the Column object. This lets us tell the containers
what kind of controls they should be using when we
change the properties that tell how many controls
they contain. For example:

Where to Define
Member Classes
In all these examples, Ive used
classes defined in PRGs,
instead of VCXs. With Pages,
OptionButtons, and CommandButtons, you can define them
either way. Headers and
Columns, though, can be
defined only in PRGs.

*!* Create Header class library


TEXT TO lcMembHeader NOSHOW
DEFINE CLASS hdrMembClass as Header
cSort = "ASCENDING"
PROCEDURE DblClick
IF This.cSort = "ASCENDING"
This.cSort = "DESCENDING"
ELSE
This.cSort = "ASCENDING"
ENDIF
MESSAGEBOX(This.cSort)
ENDPROC
ENDDEFINE
ENDTEXT
*!* Create Column class library, and pull from
Header class library

48 code-focus

TEXT TO lcMembColumn NOSHOW


DEFINE CLASS colMembClass AS Column
HeaderClass = "hdrMembClass"
HeaderClassLibrary = "membHeader.prg"
ENDDEFINE
ENDTEXT
*!* Create Grid class library, and pull from
Column class library
TEXT TO lcMembGrid NOSHOW
DEFINE CLASS grdMembClass as Grid
MemberClass = "colMembClass"
MemberClassLibrary = "membColumn.prg"
ENDDEFINE
ENDTEXT
*!* Write the libraries to disk
STRTOFILE(lcMembHeader, "membHeader.prg")
STRTOFILE(lcMembColumn, "membColumn.prg")
STRTOFILE(lcMembGrid, "membGrid.prg")

USE HOME(1) + "labels"


loform = CREATEOBJECT("Form")
loForm.NewObject("Grid1", "grdMembClass",
"membGrid.prg")
WITH loForm.Grid1
.RecordSource = "labels"
.RecordSourceType = 1
.Visible = .t.
ENDWITH
loForm.Show(1)

With every release of VFP, there is something added


thats so obvious, it tends to go in everyones
defaults. With VFP3, it was Label.AutoSize = .T.
VFP5 added ComboBox.BoundTo = .T. Now that
were up to 8, I think well see something like the
following:
DEFINE CLASS pagRefresh as Page
lRefresh = .T.

PROCEDURE Activate
IF This.lRefresh
This.Refresh()
ENDIF
ENDPROC
ENDDEFINE

I added an lRefresh property to this class because of


the possibility that you might not want a given page
to refresh as soon as you switch to it.
Once youve defined the classes, you can use them
in the Form Designer by setting the properties at
design time. When you make a design-time change
to the MemberClass property, all of the controls will
use the same class, even those already defined at
that point.
However, if you change the MemberClass and
MemberClassLibrary properties at runtime, this will
not affect existing controls. This opens up some
interesting possibilities for dynamically switching
between MemberClasses as you instantiate various
objects at runtime.
Listing 1 contains code to first change the MemberClass and MemberClassLibrary properties on a
Pageframe in (simulated) design mode, then shows
that at runtime the properties can be changed with
just the new Pages being instantiated with the
newly-defined MemberClass.
For a more detailed example, please see the
Member Classes Solution Sample that ships with
VFP8. The easiest way to do this is to use the Solution Samples pane in the Task Pane, but you can
still run SOLUTION.APP as always to get to the
samples.
Garrett Fitzgerald

Listing 1: Code that illustrates dynamic switching of MemberClass property at runtime.


*!* Create a Page in a PRG
TEXT TO lcPageClass NOSHOW
DEFINE CLASS pagRed as Page
BackColor = RGB(255, 0, 0)
Caption = "Page"
ENDDEFINE
ENDTEXT
STRTOFILE(lcPageClass, "membPagRed.prg")
*!* Create a page in a VCX
loPage = CREATEOBJECT("Page")
loPage.BackColor = RGB(0, 0, 255)
loPage.SaveAsClass("membPagBlue.vcx", "pagBlue")
*!* Open the Form Designer and get an object reference
*!* to the form.
MODIFY FORM frmDesignTest NOWAIT
ASELOBJ(laForm, 1)
loForm = laForm(1)
loForm.Themes = .F. && With the default of .T., we wont
&& see the colors
loForm.AddObject("PageFrame1", "PageFrame")
WITH loForm.PageFrame1
.MemberClassLibrary = "membPagRed.prg"
.MemberClass = "pagRed"
.PageCount = 5
.MemberClassLibrary = "membPagBlue.vcx"
.MemberClass = "pagBlue"
ENDWITH
KEYBOARD "{CTRL+W}"
*!* After this next line runs, we have blue pages, instead
*!* of red pages.
DO FORM frmDesignTest
WAIT WINDOW TIMEOUT 5
*!* Now that were at runtime, change the MemberClass
*!* properties back, and increase the page count.
WITH frmDesignTest.PageFrame1
.MemberClassLibrary = "membPagRed.prg"
.MemberClass = "pagRed"
.PageCount = 7
ENDWITH
*!* We now have 5 blue pages and 2 red ones.

Advertisers Index
CoDe Magazine
www.code-magazine.com
DevTeach
www.devteach.com
EPS Software Corp.
www.eps-software.com
Essential Fox Conference
www.essentialfox.com
F1 Technologies
www.f1tech.com
Hentzenwerke Publishing
www.hentzenwerke.com
Oak Leaf Enterprises
www.oakleafsd.com
Resoulution
www.xcase.com
Soft Classics, Ltd.
www.CodeMine.com
Stonefield Systems Group
www.stonefield.com
TakeNote Technologies
www.takenote.com

47, 53
37
2, 7, 29
19

Universal Thread
www.universalthread.com
Vision Data Solutions
www.visionds.com
West Wind Technologies
www.west-wind.com
Wizards and Builders Gmbh
www.wizards-builders.com

59
39
57, 71
5, 63

31
72

This listing is provided as a courtesy to our readers


and advertisers. The publisher assumes no responsibility for errors or omissions.

23

Advertising Sales:

33

David Stevenson david@code-magazine.com


678-575-7290

61
27
43, 45

Erna Egger erna@code-magazine.com


+43 (664) 151 0861
Tammy Ferguson tammy@code-magazine.com
281-866-7444 ext 26
code-focus

49

The VFP 8 XMLAdapter


Class
Cathi Gero
cgero@prenia.com

Cathi Gero is a consultant,


developer, and founder of
Prenia Corporation, providing
custom software application
solutions to businesses since
1987. Her expertise lies in
developing .NET business
applications, developer
consulting in .NET, and database solutions using Visual
FoxPro, SQL Server and Crystal
Reports. She is a Microsoft
MVP and a C.P.A. As a
contractor for Microsoft in
2002, Cathi was a member of
the FoxPro team working on
Visual FoxPro 8.0. She is the
Contributing Technical Editor
for the book .NET for Visual
FoxPro Developers by Hentzenwerke Publishing. Cathi has
authored whitepapers for
Microsoft and is a speaker at
many developer conferences
and user groups. Her monthly
column, Cathi Geros .NET
Tips appears in Universal
Thread Magazine and is an
example of her involvement in
the .NET community.

50 code-focus

Visual FoxPro 8.0 introduces a whole new way to work with


eXtensible Markup Language (XML). The XMLAdapter class works with hierarchical XML, provides an object-oriented approach to working with XML data,
and leverages your familiarity with tables and fields in the way it exposes the XML
contents.
any developers have come to realize the enorThe XMLAdapter class in Visual FoxPro 8.0 greatly
mous potential of a platform-neutral way to
enhances XML support to provide you with the
exchange structured data over the Intranet /
ability to work with multiple tables in one XML file,
Internet by using XML. XML makes it possible to
and the compatibility of working with XML from
integrate your applications with
different sources. The schema
others, even if theyre using platcan be modified to allow you
Fast Facts
forms and systems completely
control over how data is
different than yours. Integrating
converted to a cursor and how
The new XMLAdapter class in VFP
business
applications
has
the XML is generated from
8 greatly enhances your ability to
become an essential need. XML
Visual FoxPro cursors.
work with XML data sources,
is the key to this kind of integration.
including hierarchical XML docuIntroduction to the

ments, such as .NET DataSets.

XMLAdapter, XMLTable,
The
XMLAdapter
class
Cathi explains how this and
provides new support for
and XMLField Classes
related new classes open new
working with XML. One capapossibilities
for
your
applications.
bility of this new class is
The XMLAdapter Class allows
support for hierarchical XML.
you to load XML from an XML
This means that an XML file
source, parse the XML Schema (when it exists), and
that represents a collection of different and potenadd one or more XMLTable object(s) to its tables
tially related tables, such as a Windows .NET
collection. In turn one or more XMLField object(s)
DataSet, will render into separate Visual FoxPro
are added to the fields collection of each
cursors.
XMLTable.
Flexibility and control over data is enhanced by
XMLAdapter class can also create an XML docubeing able to control the schema of the XML that
ment representing the contained tables and fields
is created, as well as control the data types that
that have been populated. XMLAdapter class
the cursor creates from the XML of the schema.
includes two other child member classes:
This allows you to load in the XML, change the
XMLTable and XMLField. These provide the ability
schema, then generate the cursor. In addition,
to walk through the schema programmatically and
you can take a cursor in memory, control the
access or set information.
schema, then generate the XML in a different
format.
The primary functionality that the XMLAdapter
Class provides is to retrieve XML via the
Working with XML in Visual FoxPro 8.0
LoadXML() method, then parse the XML via the
vs. Visual FoxPro 7.0
contained XML Schema, as appropriate, into one or
more XMLTable objects, which in turn contain
The XMLTOCURSOR() / CURSORTOXML() funcXMLField object(s).
tions that were new to Visual FoxPro 7.0 restricted
you to working with XML files that contained data
The XMLTable class is a collection of all tables
for only one table. If more than one table is
contained in the XML and functions to allow you to
contained in the XML file, you need to parse
step through the table to perform procedures on
through the file manually. In addition, you dont
them.
have the full control of the schema that was
contained in the XML file to change the data types
The collection of XMLTable objects describes the
before converting the XML to a Visual FoxPro
XML as a Visual FoxPro cursor or cursors, along
cursor. When the XML was generated from the
with any relational information. The XMLAdapter
cursor, you dont have control of the schema that is
does not store the actual XML schema or content,
generated.
but does store object references to them.

The developer may then use the XMLTable.ToCursor() method to produce a cursor that contains the
data of all the fields represented by the child
member XMLField collection.
The XML and XML Schema data retrieved via the
XMLAdapter.LoadXML() method remains in
memory until replaced via a subsequent call to
LoadXML(), or when it is specifically released by
calling the ReleaseXML() method.
The XMLField class is a collection created for
each XMLTable and contains all the fields in the
table. The developer can iterate through the field
objects and make any necessary changes. There
are no methods associated with the XMLField
class.

Converting XML to VFP Cursors Using


the XMLAdapter
Now lets put the XMLAdapter class to work and
see how easy it is to take an XML file and convert it
to a Visual FoxPro cursor. Well work with an
XML file representing Customer data, which
contains one table with four records, each
containing two fields.
The code to read the XML and create a cursor
using the XMLAdapter class is as follows:
cFile = "c:\XMLAdapter\CustomerXML.xml"
adapter = CREATEOBJECT("XMLAdapter")
adapter.LoadXML(cFile,.T.)
adapter.Tables(1).ToCursor()

First, a reference is made to the XML file. Next, an


instance of the XMLAdapter class is created. The
LoadXML method is then called, loading the XML
from the file into a Document Object Model
(DOM) document and attaching it to the
XMLAdapter object. The first parameter is the
name of the XML file or the string which contains
the XML. The second parameter determines if the
first parameter represents a file.
Finally, the collection of XMLTable objects is
accessed. Since there is only one table contained
in the XMLAdapter object, we can directly access
the XMLTable. Lastly, the ToCursor method is
called to convert the XML to a Visual FoxPro
cursor. See Figure 1 for an example of the cursor
created.

Figure 1: Cursor created from the XML file.

Converting Cursors to XML Using the


XMLAdapter Class

From the VFP Team:

Now that we have seen how easy it is to create


Visual FoxPro cursors from XML, lets look at how
easy the XMLAdapter class makes it to perform the
reverse: creating an XML file from a cursor. The
following example will select a set of records from
the Employees table in the Northwind database that
now ships with Visual FoxPro 8.0 and create an
XML file from the cursor:
cFile = "c:\XMLAdapter\EmployeeXML.xml"
OPEN DATABASE (_samples+"\northwind\northwind")
SELECT employeeid, lastname ;
FROM Employees;
INTO CURSOR curEmployees
adapter = CREATEOBJECT("XMLAdapter")
adapter.AddTableSchema("curEmployees")
adapter.ToXML(cFile,,.T.)

Above, the name of the XML file is defined and the


database is opened. The cursor is created to extract
records from the Employees table and an instance
of the XMLAdapter class is created. To add a
cursor to the XMLAdapter object, the AddTableSchema method is called, passing in the name of
the cursor. Finally, the ToXML method is called to
create the XML file.
The first parameter is your desired file name for the
XML file. The second parameter allows you to
optionally specify an external schema file. In this
example, the schema will be generated inside of the
XML file, called an inline schema; therefore this
parameter is skipped. The third parameter is passed
as a true to specify that the first parameter is a file
name used to generate the XML in an external file
instead of into a string.

Working with XML Schemas


XML Schemas are a standards-based format of the
W3C (World Wide Web Consortium) for defining
the structure of XML data. A schema is an XML
document that defines the structure, constraints,
data types, and relationships of the elements that
constitute the data contained inside the XML document or in another XML document.

Aleksey Tsingauz
Developer,
Visual FoxPro Team
Microsoft
The new XMLAdapter class in
VFP 8.0 converts XML to VFP
cursors and vice versa in a
much easier and more flexible
way than the VFP 7.0 CursorToXML and XMLToCursor functions. This new class includes
support for converting back
and forth between hierarchical
XML and multiple cursors as
well as the ability to preserve or
apply changes via .NET DiffGrams. This allows even
greater compatibility and functionality for VFP developers
when working with remote
data, SQL Server, and XML
Web services. With the
XMLAdapter class, working
with XML in VFP has never
been more flexible, easy,
and fun.

Schemas are written in a specialized syntax, and


simply specify the elements and attributes that are
allowed in a particular document. You can use
schemas not only to define the valid elements and
attributes of a document, but also to specify data
types. Schemas can be included inside of the XML
file and are called inline schema. Alternatively, they
can be an external file which uses the XSD or XDR
extension.
Although in most cases you will not need to
directly work with schemas, it is important to
understand that they provide information about
data types. As you will see further along in this
article, sometimes the data types need to be
altered while working with XML in Visual FoxPro.

code-focus

51

The XMLAdapter class requires MSXML 4.0 SP1 as


a minimum. MSXML 4.0 SP1 is installed with
Visual FoxPro 8.0.

XML Schema Formats


Schemas are written in a
specialized syntax, and simply
specify the elements and attributes that are allowed in a
particular document.
The XMLAdapter class supports
the following XML Schema
Formats:
XS (W3C XML Schemas) as
used with Windows .NET
DataSets, SQLXML SoapOn-The-Server, and as
produced by Visual FoxPro
8.0 XMLAdapter.ToXML()
and XMLAdapter.ApplyDiffgram() methods.
XDR (Microsoft XML Data
Reduced Schema) as implemented by SQLXML.
ADO Recordset Schemas

XML Templates for


SQL Server queries
XML templates give you a more
controlled security environment
than simply allowing users to
send queries in a URL to a
server for data retrieval.
Templates contain one or more
SQL Server queries, the results
of which are sent to the calling
browser or client application.
Because the client receives only
the query results and not the
source code for the query, the
data access logic is encapsulated. Clients would not see the
SQL used to access the data;
they would see only the
resulting query results.

The XML Schema Object Model (SOM) is a rich


API allowing you to create, edit, and validate
schemas programmatically. SOM operates on
schema documents analogously to the way Document Object Model (DOM) operates on XML documents. Working with the object model of the
XMLAdapter class eliminates the need to work with
the SOM directly, providing granular control when
working with schemas.
Data types can be changed within the XMLFields
collection prior to calling the XMLAdapter.ToCursor() method to generate a Visual FoxPro cursor.
One instance of needing this kind of control would
be where an XML file contains an element that
stores a zipcode as a numeric, yet the cursor you
generate needs the data as character type. Modify
the data type using the following example code:
adapter = CREATEOBJECT("XMLAdapter")
cFile = "c:\XMLAdapter\EmployeeXML.xml"
adapter.LoadXML(cFile,.T.)
adapter.Tables(1).Fields(1).DataType = "C"
adapter.Tables(1).Fields(1).MaxLength = 10
adapter.Tables(1).ToCursor()

The XMLAdapter class allows you to create your


own schema to manually customize an XML file.
Additionally, you can use an XMLAdapter object to
read the schema in order to understand the file
structures of the contained data.

XMLAdapter Class with SQLXML


The XMLAdapter class supports SQL Server 2000
queries that output to XML using the SQLXML
SDK (see sidebar, The SQLXML SDK). The
following is an example of a T-SQL query that
generates XML for the result set:
SELECT Customers.CustomerID,Customers.CompanyName,
Orders.OrderID,Orders.OrderDate,
Orders.ShipName,Order_details.ProductID,
Order_details.UnitPrice,Order_details.Quantity
FROM Customers
Inner Join Orders
On Customers.CustomerID = Orders.CustomerID
Inner Join [Order Details] Order_details
On Orders.OrderID = Order_details.OrderID
WHERE Customers.CustomerID = CACTU AND
Orders.OrderID = 10521
FOR XML AUTO, ELEMENTS, XMLDATA

The XML generated pulls data from three different


tables and the XML data maintains the relationships between these tables.
This allows an
XMLAdapter object to maintain the parent-child

52 code-focus

Integrating business
applications has become an
essential need. XML is the key
to this kind of integration.

For more information about schemas, visit


http://www.w3.org/XML/Schema.

relationships by populating the XMLTable.ParentTable and XMLTable.ChildTable properties. The


following Visual FoxPro code uses the XML file to
display the relationships between the different
XMLTable objects:
adapter = CREATEOBJECT("XMLAdapter")
cFile = "c:\XMLAdapter\SQLXML.xml"
adapter.LoadXML(cFile,.T.)
oTable1 = adapter.Tables(1)
oTable2 = oTable1.ChildTable
oTable3 = oTable2.ChildTable
oTable4 = oTable3.ChildTable
? oTable1.Alias && Customers
? oTable2.Alias && Orders
? oTable3.Alias && Order_details
? oTable4
&& .NULL.
oParent1 = oTable1.ParentTable
oParent2 = oTable2.ParentTable
oParent3 = oTable3.ParentTable
? oParent1
&& .NULL.
? oParent2.Alias && Customers
? oParent3.Alias && Orders

When the ToCursor() method is called, only one


cursor (called Customers) will be generated, representing the join of all three tables. One note to
keep in mind: The XML created above does not
generate a schema which sets the maxlength for
string fields. Therefore, Visual FoxPro will convert
the data type to memos, since that is the default
data type for unlimited string lengths. To work
around this, you will need to modify the schema
before calling the ToCursor() method. Here is the
code:
oTable1.Fields(1).DataType = "C"
oTable1.Fields(1).MaxLength = 6
oTable1.Fields(2).DataType = "C"
oTable1.Fields(2).MaxLength = 40
oTable2.Fields(3).DataType = "C"
oTable2.Fields(3).MaxLength = 40
adapter.Tables(1).ToCursor()

A use for SQLXML is to publish SQL Server data


for Intranet and Internet-based applications using
the HTTP publishing functionality. The ability to
publish data over HTTP allows you to build highly
data-centric Web sites.
In addition, the
XMLAdapter class can retrieve data as XML by
simply making an HTTP request. The request can
be a select statement or a call to a XML template
(see sidebar, XML Templates).

Dim cConnStr As String = _


"user id=sa;Initial Catalog=Northwind;Server=(local)"
Dim strSQL As String
Dim ds As New DataSet()
strSQL = "SELECT CustomerID, CompanyName " & _
"FROM Customers " & _
"WHERE CompanyName LIKE A%"
Dim da_customers As New SqlDataAdapter(strSQL, cConnStr)
Include all length information in this schema if a VFP client
is consuming this data via an XMLAdapter otherwise all the client
cursor fields will be memo fields. This is because the memo field
is the default type in VFP for unlimited length strings.
da_customers.MissingSchemaAction = MissingSchemaAction.AddWithKey
da_customers.Fill(ds, "Customers")
strSQL = "SELECT Orders.CustomerID, Orders.OrderId, " & _
"Orders.OrderDate, Orders.ShipName " & _
"FROM Orders INNER JOIN Customers " & _
"ON Orders.CustomerID = Customers.CustomerID " & _
"WHERE Customers.CompanyName LIKE A%"
Dim da_orders As _
New SqlDataAdapter(strSQL, cConnStr)
Include all length information in this schema if a VFP client
is consuming this data via an XMLAdapter otherwise all the client
cursor fields will be memo fields. This is because the memo field
is the default type in VFP for unlimited length strings.
da_orders.MissingSchemaAction = MissingSchemaAction.AddWithKey
da_orders.Fill(ds, "Orders")
strSQL = "SELECT Order_details.OrderId, " & _
"Order_details.ProductID, " & _
"Order_details.UnitPrice, Order_details.Quantity " & _
"FROM [Order Details] Order_details " & _
"INNER JOIN Orders " & _
"ON Order_details.OrderID = Orders.OrderId " & _
"INNER JOIN Customers " & _
"ON Orders.CustomerID = Customers.CustomerID " & _
"WHERE Customers.CompanyName LIKE A%"
Dim da_orders_details As _
New SqlDataAdapter(strSQL, cConnStr)
Include all length information in this schema if a VFP client
is consuming this data via an XMLAdapter otherwise all the client
cursor fields will be memo fields. This is because the memo field
is the default type in VFP for unlimited length strings.
da_orders_details.MissingSchemaAction = _
MissingSchemaAction.AddWithKey
da_orders_details.Fill(ds, "Order_Details")
Dim strFileXML As String = "c:\XMLAdapter\DotNET.xml"
Dim strFileXSD As String = "c:\XMLAdapter\DotNET.xsd"
ds.WriteXmlSchema(strFileXSD)
ds.WriteXml(strFileXML)

54 code-focus

Working with Windows


.NET Framework
In Microsoft .NET Framework classes, the primary
class used for manipulation of data is the Windows
.NET DataSet. The DataSet is a class that encapsulates data as XML. XML may be returned from
DataSets in several ways:
Return the entire Windows .NET DataSet to the
calling application, which returns all rows in DiffGram format with inline schema, having Updates,
Inserts, and Deletes indicated.
Return Windows .NET DataSet changes only
which returns only the rows that have been modified, added, or deleted in Diffgram format with
inline schema.
Windows .NET DataSet class supports the
DataSet.GetXml and GetXmlSchema methods
which return XML to a .NET string type.
Windows .NET DataSet class supports the
DataSet.WriteXml
and
WriteXmlSchema
methods which write the DataSet as XML with
Inline Schema, without Schema, or with Schema
separately.
A great amount of focus went into making the
XMLAdapter class compatible with Windows .NET
DataSets. The XMLAdapter class supports hierarchical XML format, which improves Visual
FoxPros ability to interoperate with XML produced
from and written to Windows .NET DataSets.
Separate Visual FoxPro cursors will be created for
each DataTable contained in the DataSet.
Listing 1 shows sample Visual Basic .NET code that
retrieves data from three SQL Server tables into one
Windows .NET DataSet, then exports the DataSet
as XML to an XML file and the schema to a separate XSD file. One note worth mentioning when
working with Windows .NET DataSets: You must
set the property MissingSchemaAction of the
DataAdapter class prior to filling the DataSet; like
the following:
da_customers.MissingSchemaAction = _
MissingSchemaAction.AddWithKey

This is required to include all length information in


the schema. Otherwise, all the .NET Framework
string fields will be modified to Memo fields in Visual
FoxPro. This is because the memo field is the default
type in Visual FoxPro for unlimited length strings.

The XMLAdapter class supports


hierarchical XML format, which
improves Visual FoxPros ability
to interoperate with XML
produced from and written to
Windows .NET DataSets.

Listing 1: Sample .NET Code

When working with Windows


.NET DataSets, you must set the
property MissingSchemaAction
of the DataAdapter class prior to
filling the DataSet

provide an efficient way of transferring data back


and forth between applications. Diffgrams are a
special type of an XML document that are
formatted to only include information for changed
data, rather than the entire set of data. Using this
format can reduce the size of the XML that needs to
be sent between applications.

Figure 2: Visual FoxPro cursors created from a Windows


.NET DataSet.
The XMLAdapter class can consume this XML by
reading in the data and generating three different
cursors. Here is the code:
adapter = CREATEOBJECT("XMLAdapter")
cFile = "c:\XMLAdapter\DotNET.xml"
cXSDFile = "c:\XMLAdapter\DotNET.xsd"
adapter.XMLSchemaLocation = cXSDFile
adapter.LoadXML(cFile,.T.)
FOR EACH oXMLTable IN adapter.Tables
oXMLTable.ToCursor()
ENDFOR

Three different cursors will be created, as shown in


Figure 2. The schema information for this example
was created as an external file using Visual Basic
.NET. When you have an external file, you need to
set the XMLSchemaLocation property of the
XMLAdapter object to the XSD file prior to reading
in the XML.
One limitation of the XMLAdapter class when
working with Windows .NET DataSets is that no
relationships that are set in the DataSet are
imported into the XMLAdapter class. You will
need to set any relations between the generated
cursors manually. The same is true when you
export XML from the XMLAdapter that is to be
imported into a Windows .NET DataSet. The relationships between the cursors are lost.

Working with Diffgrams


The XMLAdapter class supports Diffgrams, which

The developer loads the XML into Visual FoxPro,


converts it to a cursor, makes the necessary updates
to the data, and converts the data back to a XML
document Diffgram. Only the changed data will be
returned to the receiver of the XML. This XML
document Diffgram can not only be used to apply to
another XMLAdapter class in Visual FoxPro but
also can be used in other applications that support
data in Diffgram format. These XML formats are
compatible with ADO.NET for use in Windows
.NET Framework applications.
The Windows .NET DataSet is always serialized to
XML in Diffgram format when the DataSet is
returned to Visual FoxPro. The sample Visual Basic
.NET code below returns a Windows .NET DataSet:

The SQLXML SDK


Microsoft enhanced SQL Server
2000 by providing a SQLXML
SDK, which provides support
for updating and retrieving data
in XML. The current version is
SQLXML 3.0 SP1. These
updates reflect the growing
maturity and industry acceptance of XML and the subsequent advances in XML-related
standards and technology. By
enhancing the support for XML
in SQL Server 2000, Microsoft
has made it easier to integrate
SQL Server-based solutions
with existing systems, Web
applications, and business partners. The SQLXML SDK can be
downloaded from
http://microsoft.com/sql/downloads.

Return MyDataSet

If the DataSet contains changes, they will be flagged


via the Diffgram:hasChanges="modified" or Diffgram:hasChanges="inserted" attributes. For "modified" records, the previous values will be in the
diffgr:before section. Deleted records appear only in
the diffgr:before section and not in the main section
of the Diffgram. Either the entire Windows .NET
DataSet (all the rows in the original query), or a
DataSet that only contains changes, can be
returned.
In the case where all rows are returned, unchanged
rows will not have a diffgr:hasChanges attribute. To
load the DataSet into an XMLAdapter object, you
need to use the Attach() method instead of the
LoadXML() method since the DataSet returns a
XML DOM object. Here is sample Visual FoxPro
code:
adapter = CREATEOBJECT("XMLAdapter")
* Call a .NET Web Service to return a DataSet
DataSetadapter.Attach(MyWebService.FetchData())

You can create XML in Diffgram format using the


XMLAdapter class. This next example creates a
read-write cursor that contains customer informa-

code-focus

55

UpdateGrams and
DiffGrams
SQLXML can also be used to
output XML in updategram
format. Visual FoxPro 7.0
provided support for updategrams with the XMLUPDATEGRAM function. The new
CursorAdapter class in Visual
FoxPro 8.0 also supports
updategrams.

The XMLAdapter class does not


support this format. Instead,
the Diffgram format (which is
also supported starting in
SQLXML version 2.0) must be
used. Updategram functionality
was added to SQL Server 2000
in SQLXML 1.0. It provides a
way for you to perform database update operations by
creating XML documents that
contain the necessary before
and after images of the data
being modified.

An updategram is based on the


xml-updategram namespace
and contains one or more sync
elements. Each sync element
represents a transactional unit
of database modifications.

XMLAdapter Solution
Sample Utility
Visual FoxPro 8.0 provides a
solution sample that allows you
to work with XML using the
XMLAdapter class. This tool
can be used to test various
property settings and to
generate various types of XML.
You can load XML files for
compatibility testing with the
XMLAdapter class. The solution sample is called Loading
and Generating XML Using
XMLAdapter Class. To open
the solution sample, use the
new Task Pane Manager.

tion from the new Northwind database (included


with Visual FoxPro 8.0) and exports the changed
record to a Diffgram XML file:
cFile = "c:\XMLAdapter\DiffgramXML.xml"
OPEN DATABASE (_samples+"\northwind\northwind")
SELECT CustomerID, CompanyName ;
FROM Customers;
INTO CURSOR curCustomers READWRITE
CURSORSETPROP("Buffering", 5)
LOCATE FOR CustomerID = "AROUT"
REPLACE CompanyName WITH "Around the Corner"
adapter = CREATEOBJECT("XMLAdapter")
adapter.AddTableSchema("curCustomers")
adapter.IsDiffgram = .T.
adapter.ToXML(cFile,,.T.,.T.,.T.)

Table buffering is turned on to allow the


XMLAdapter object to detect the before and after
values for the altered records. A record is then
changed to replace the CompanyName field with a
different string. The XMLAdapter object is instantiated and the cursor is added to the XMLTable
collection. The property IsDiffgram is set to true
to allow the exported XML to be formatted for Diffgrams.
Finally, the ToXML() method is called to export the
XMLTable to an XML file. Two new parameters
are used in this example: The fourth parameter
specifies whether to include the diffgr:before section
in the Diffgram; and the fifth parameter specifies
whether the XML should contain changes only.

Working with XML Web Services


An XML Web service is an application or block of
executable code that is hosted on a web server and
whose methods are exposed through standard XML
protocols to be executed over HTTP. Those
methods can be from any platform, from any
language on either side. XML Web services are an
industry standard for communicating between
applications over the Intranet/Internet; and XML is
the meta-language that serves as the backbone for
these services.
As an example, you might use the XMLAdapter to
retrieve data from a Windows .NET Framework
application via an XML Web service.
The
Windows .NET Framework application fetches data
using a Visual FoxPro COM object that uses the
XMLAdapter class to prepare the data to export in
XML Diffgram format. The data is then exposed to
other applications, such as other Visual FoxPro
applications, mobile devices, .NET Webforms, and
.NET Winforms, by using an ASP.NET XML Web
service to pass the data in the format of XML to the
calling applications.
The Visual FoxPro application uses the
XMLAdapter class to accept XML sent by the
consumers of the XML Web service and updates
the database with the changes. The ApplyDiffgram()

56 code-focus

method is called on the XMLTable object to apply


the changes to the cursor, as named in the Alias
property to which it applies. Shown below is the
code that accepts an XML Diffgram file and
updates the Customers table with the changes:
OPEN DATABASE (_samples+"\northwind\northwind")
cFile = "c:\XMLAdapter\DiffgramXML.xml"
adapter = CREATEOBJECT("XMLAdapter")
adapter.LoadXML(cFile,.T.)
adapter.Tables(1).Alias = "Customers"
USE Customers IN 0
adapter.Tables(1).ApplyDiffgram()

Working with the new CursorAdapter


Class
Using the XMLAdapter class with the new
CursorAdapter class provides more control over the
XML data versus simply using the CursorAdapter
alone. Data types can be changed prior to generating the XML or prior to filling cursors. If the
XML file to be imported contains more than one
table, the XMLAdapter creates different XMLTable
objects.
The XMLAdapter class then offers flexibility as an
XML source for CursorAdapter objects by allowing
you to specify a valid XMLTable object as the argument for the CursorAdapter.SelectCmd property.
The code below demonstrates using the
CursorAdapter:
adapter = CREATEOBJECT("XMLAdapter")
cFile = "c:\XMLAdapter\CustomerXML.xml"
adapter.LoadXML(cFile,.T.)
MyCursorAdapter.SelectCmd="adapter.Tables(1)"
MyCursorAdapter.CursorFill()

If you are planning to use a CursorAdapter to


communicate with an XML Web service via the
Soap Toolkit in Visual FoxPro, you will need to use
the XMLAdapter class to retrieve the XML from the
XML Web service. Do this by using XMLAdapter.Attach() to retrieve a Windows .NET DataSet.

Summary
XML provides a universal way of describing,
exchanging and moving data over the
Intranet/Internet. It is platform-neutral. XML is a
basis for Windows .NET technologies. The strategic
value of Visual FoxPro 8.0 supporting and
embracing XML is that it gives Visual FoxPro the
capability to connect to other Visual FoxPro applications and Windows .NET applications, as well as
other platforms or systems that support XML
universally integrating business applications.

Cathi Gero

code-focus

57

From the VFP Team:

My Favorite Feature
When you first begin using the new Visual FoxPro 8, you are sure
to find useful new features that will make your development tasks
easier. Several members of the Visual FoxPro developer community who have already worked with VFP 8 tell us their opinions of the best and most useful new features. Perhaps their answers will help guide you to some cool ideas you can put to
work right away.

Richard Stanton
Developer,
Visual FoxPro Team
Microsoft
Beyond new features, product
stability was one area we really
focused on for Visual FoxPro
8.0. We fixed many bugs that
existed in previous versions of
FoxPro. In addition to coding
some of the great new VFP 8.0
features, I spent a great deal of
time fixing bugs reported in the
beta and from previous
versions of VFP. We are confident that VFP 8.0 is the most
stable release ever, and it is a
great upgrade from VFP 7.0
SP1. Besides, I always think of
stability as a feature.

58 code-focus

3 Cool New Tools

CursorAdapter Gets My Vote

Tamar E. Granor
Technical Editor, FoxPro Advisor
Author, Whats New in Visual FoxPro 8

Mark McCasland
US Environmental Protection Agency
M & J Software
www.mctweedle.com

m really excited about the three new tools that


VFP 8 offers: Code References, the Toolbox and
the Task Pane Manager. Each makes it easier to
get things done fast.

The most exciting new feature in VFP8 for me is the


CursorAdapter class. This class can be used to
access native VFP data or remote data from SQL
Server, Oracle and any other SQL database via
ODBC or ADO provided there is a driver or
provider for the DB. The companion to this is the
ability to create a shared connection for any remote
data (including remote views) using the
SQLEXEC() or SQLSTRINGCONNECT() functions.

The Code References tool provides a projectoriented search-and-replace tool. It also provides
the engine for the new View Definition item on the
context menu of editing windows, which lets you
quickly move to the definition of the highlighted
item (variable, property, method, class, constant,
etc.). Ive already used this tool extensively to
Even if you are not yet developing for a SQL dataexplore the code of all three tools, as well as for
base, the CursorAdapter will
searching in a multi-developer
allow you to specify a NATIVE
client project. It makes underFast Facts
DataSourceType so you can
standing the structure of a
handle your VFP data like a
project much easier.
We asked for input from several
SQL database. Then, if you
early adopters of VFP 8 about
ever have to upsize the dataThe Toolbox strikes me as a
their favorite new features.
base to SQL Server or Oracle,
marriage of the Form Controls
Youll sense their enthusiasm as
all you have to do is change the
toolbar and the Component
you read their suggestions for
*DataSource and *DataSourceGallery. Its more capable than
why you should step up to the
Type properties in each Cursothe toolbar and much easier to
rAdapter.
understand and use than the
latest and greatest version.
Gallery. It provides a home for
Get used to seeing the *Propall the controls you use, as well
erty
shorthand
notation
as for blocks of text (such as a
because there are three prefixes associated with
standard header). The ability to add my own cateeach suffix preceded by an asterisk. They prefixes
gories and items means that I can put everything Ill
are always INSERT, UPDATE and DELETE. For
need for a given project in one easy-to-find place.
example, *Cmd is short for the InsertCmd,
UpdateCmd and DeleteCmd properties. Then there
Im most intrigued by the possibilities of the Task
is the same trio set for *CmdDataSource and
Pane Manager. This tool provides a portal for VFP
*CmdDataSourceType.
development. The panes provided include a way to
centralize access to all my regular development
I can now create a generic subclass of this
tools, offer quick access to sample code, let me
object which I will then use to create a subclass
organize paths and settings, and more. But the Task
for each remote table in a database. In fact, develPane Manager doesnt stop there. You can add
oping a builder to do this for you is quite simple.
third-party panes and create your own. Ive already
Using my builder, I loop through an array of
added a pane giving me quick access to my
tables and get the column and data type meta data
Hentzenwerke e-books, and another that displays
from the SQL database for each table, which I
my own website. I expect the VFP 8 versions of
then use to populate the following properties for
most VFP tools to include custom task panes, so
the Adapter:
you can switch between them easily.

Alias
ConversionFunc
KeyFieldList
SelectCmd
Tables
UpdatableFieldList
UpdateNameList
I cheat a little for the KeyFieldList property because
ALL my tables use a surrogate integer field for the
primary key, and it is always the same name,
KEYID. However, I can also retrieve the PK
column for any table from the SQL database to
make this even more generic.
The ConversionFunc property lets you specify what
conversion function to perform on a particular field.
For example, for VARCHAR data types, VFP in the
past would send data right padded with spaces.
Well, that defeated the purpose of a VARCHAR
column since all these spaces were not RTRIMmed
before being sent to the database unless you
handled all the INSERT or UPDATE SQL yourself.
Now, you can list such fields and how to format the
data before it goes to the database. For example:

with a converted Northwind VFP database, and my


examples show how the Adapter works with both
databases.
You can also download my CursorAdapter builder at
http://www.mctweedle.com/downloads/cabuilder.zip.

How I Will Use BINDEVENT()


Drew Speedie
Contributing Editor, FoxPro Advisor
Magazine
Architect of the Visual MaxFrame
Professional framework
The new BINDEVENT() function is what Im most
excited about of all the new VFP 8.0 features. Here
are some of the ways Im working on implementing
it in the Visual MaxFrame Professional (VMP)
framework.
Create a handler for _Screen: You cant subclass
the _Screen object, but you can now
BINDEVENT() its methods and properties to a
custom screen handler.

loAdapter.ConversionFunc = ;
[LastName RTRIM, FirstName RTRIM]

When you create the SQL for the UpdateCmd or


InsertCmd properties and are going to trim your
character fields, it does make a difference where the
? goes. For example:
UPDATE Customers SET LastName =
RTRIM(?crsCustomers.LastName) WHERE
UPDATE Customers SET LastName =
?RTRIM(crsCustomers.LastName) WHERE

In the first UPDATE, RTRIM is included in the


SQL passed to the server, so RTRIM must be a
valid function on the server. For Oracle and SQL
Server, this is valid. However if you just used
TRIM() or ALLTRIM(), you would generate a
server error. In the second UPDATE, the value of
crsCustomers.LastName is evaluated at the client,
and only the trimmed value is passed to the server.
So the use of TRIM(), ALLTRIM() or any other
valid VFP function will not matter.
As you can see, the CursorAdapter can be a very
powerful object, and I did not even touch upon its
numerous events and methods that let you have a
little or as much control over the handling of the
data as you want. I hope this gives you an idea of
the excitement this new class has generated in the
Visual FoxPro community. It has already become a
very important component in my toolbox.
If you are interested in detailed examples of its use
that you can use today [if you have VFP8], you can
download them from my web site, http://www.
mctweedle.com/downloads/CursorAdapter.zip. It also
helps to have MSDE or SQL Server with the Northwind sample database or Oracle. VFP 8 now comes

code-focus

59

BINDEVENT() the _Screen.Resize event to the


screen handler object. Whenever the user resizes
_Screen, the screen handler code fires to reposition
a wallpaper graphic at the desired location in
_Screen, along with anything else you want to
happen when _Screen is resized either programmatically or interactively.
Its a snap to create a shortcut menu for the _Screen
surface itself, by BINDEVENT()ing to _Screen.
RightClick(). This and the previous item are what I
call runtime subclassing adding behaviors to the
method of an existing object at runtime.
If need be, you can even implement runtime access
and assign methods. Just BINDEVENT() the
_Screen.Caption property to your custom screen
handler.
For
assign-style
functionality,
BINDEVENT() to _Screen.Caption once without
the optional nFlags parameter to save the Caption
value before it is updated, and BINDEVENT() a
second time, passing the nFlags parameter as 1,
where you can take action based on the newlyassigned _Screen.Caption while having access to its
original value saved in the first BINDEVENT().
BINDEVENT() _Screen.MouseMove() to take
action when the mouse moves over a particular area
of the screen. Use this idea to confuse the heck out
of the users of your application, by firing random
application events. Or try more useful (but less fun)
things, like making a particular corner of _Screen
hot when the mouse passes over it. Ive setup the
extreme upper-right corner of my _Screen during
development to fire up the Toolbox, as a poor mans
simulation of a flyout behavior.
Eliminate the need for tightly-coupled messages
between objects: A long time ago I abstracted hardcoded calls in the VMP framework so that forms
automatically message instances of a special
container class whenever the user initiates basic
actions - Add, Delete, Save, Cancel, etc. Then
container class registers with the form on instantiation, so the form can message them without iterating through all the members of the form.
While Im not planning to take the time to rip out
that code just because I can, from now on I can
have ANY control participate in those actions
simply by having them BINDEVENT() to the
desired form methods. The form doesnt have to
know they exist.

60 code-focus

the sort order ascending/descending.


The grid can determine which header called via
AEVENTS(), passing the 2nd parameter as zero.
But since you will most likely create column and
header classes now that VFP 8.0 makes that much
easier, the BINDEVENT() can be done by the
columns and headers as they instantiate, rather than
the grid having to iterate through them when it
instantiates.
Handle object reference cleanup in forms: When
form members store object references to custom
properties, its critically important that those object
references be released no later than the
Form.Destroy(). This has always been problematic
because the Form.Destroy() fires BEFORE the
Destroy() of each of its members, making it impossible for the members to handle object reference
cleanup in their Destroy().
In VFP 8.0, just BINDEVENT() a custom ObjectRefCleanup() method to the Form.Destroy(),
passing the 5th nFlags parameter as 0 to ensure the
member.ObjectRefCleanup() code fires before the
Form.Destroy() finishes.

Member Classes Are Great!


Barbara Peisch
Peisch Custom Software, Inc.
www.peisch.com
My favorite new feature in VFP 8 is Member
Classes. It isnt a feature thats receiving a lot of
press, but I think its one that most developers really
want. The best examples I can think of that makes
Member Classes so great are ones using PageFrames. Up until now, if you wanted to use your
own page subclass on your pageframes, you had to
define that page class in code, and couldnt use it in
the class designer.
You then had to have code in your PageFrames Init
to add the pages at runtime. Bleh! Now you can
visually design a page subclass, and by specifying
that page subclass as the member object in your
PageFrame base class, all your PageFrames will use
your subclass instead of the native page class.

Eliminate the need for column and header


classes: The biggest reason I want to subclass
columns and headers is to implement behaviors, not
set properties. Now the grid can just iterate through
its member columns and their headers,
BINDEVENT()ing events to grid methods where all
the actual work is done.

Using PageFrames, there are two common examples


I can think of that make this very useful. The first is
if you want the characteristics of the selected page
to change. Previously, we had to do this using the
UIEnable method of a custom class dropped onto
each page. Now, you can just put that code into the
page itself. For example, you can change the ForeColor and BackColor of a page in its Activate
method, and change it back in the Deactivate
method.

It doesnt matter what column/header classes are


instantiated, and even the VFP base classes work
fine. For example, the RightClick of a header can
delegate to a custom HeaderRightClick method of
the grid, which can handle something like toggling

Another example is putting a This.Refresh() in the


pages Activate method. I dont know about you, but
most of the time, thats the behavior I want, except I
forget to add it to the page until I have a problem.
Now, Ill never have to worry about that again!

The Task Pane and the Environment


Manager
Paul Maskens
pmaskens@mvps.org
Well, everyone seems to be talking about the new
OO features and new Data handling classes. Im
different, my favorite (and its cool) is the Environment Manager in the Task Pane. I often end up
working on several different projects at once, and
this helps organize them, and me! Its so simple to
use, too. Open the Task Pane (Tools|Task Pane on
the menu) and click on the Environment Manager
button. Then click on the Manage Environments
link. Thats it! It really is so simple to use. So simple
that theres no help for it.
In fact, the whole Task Pane is a neat VFP8 feature,
extensible and of awesome flexibility. The combination of VFP, Explorer, JavaScript, XML, XSL, and
HTML is amazingly powerful.
If you want to dip into it, click on the Options
button in the Task Pane. Then click on Task Pane
Manager in the treeview on the left. Click on
Customize under that. Finally click on the
Customize Panes button. Just take a look at how its
put together and what you can do with it.
Click on Environment Manager in the list on the
left. The General tab isnt very interesting, but
click on the Data tab. Here you find what makes it
work:
<?xml version=1.0 encoding=utf-8
standalone=no?>
<?xml:stylesheet type="text/xsl"
href="envmgr.xsl"?>
<VFPData>
<!-- CONTENT -->
</VFPData>

How? Well the key is in the stylesheet applied to this


very simple XML, in the envmgr.xsl file. Click on the
View Files option button at the top of the screen (yes,
I missed seeing that for a few minutes, too!). Then
open the xsl file just by clicking on it in the list.
This is the magic line in the .XSL file:
<A href="vfps:doapplication?filename=(HOME() +
[envmgr.app])&amp;refresh">Manage Environments</A>

Any URL with a vfps: prefix is passed back to the


handler in the Task Pane application. So when you
click on the Manage Environments link, the application is run with the parameter refresh. Youll
need to take a look at home(7)+taskpane.dbf and
home(7)+panecontent.dbf to see the data that
drives the TaskPane application.
Unfortunately, the source code for envmgr.app isnt
in the XSource.ZIP file that ships with VFP8, so

theres a piece of the puzzle missing. But the code


for the Task Pane application itself is there.

The Code References Search Tool


Cindy Winegarden
MCSD, Microsoft Visual FoxPro MVP
cindy.winegarden@mvps.org

The Visual FoxPro Code References Window is one


of the great developer productivity tools the Fox
team added in Version 8. Use it when you need to
spelunk through unfamiliar code or when youre
making changes and need to evaluate every place a
particular text string is used.
I found this tool invaluable when I was working
with the new VFP Toolbox source code. The code
for the _Root class, which is the Foundation of the
Toolbox, is at least 30 pages. The code references
tool made it easy for me to trace references to
various objects through that mountain of code.
The Code References tool allows you to specify a
search string, choose a project or directory to
search, and choose any of several groups of file
types to search in. Once youve got your results, you
can double-click to open the item containing the
text string; when the window comes up the string
will be highlighted. Also, you can choose to replace
the text with some other string, and the Code References Window can be configured to verify and log
each change and create a backup of any file that is
changed.
The details of past searches and replacements are
stored in the RefAddIn.dbf, RefDef.dbf, and
RefFile.dbf tables in the HOME(7) or user application data directory, and until you Clean Up Source
Tables you will have access to past searches each
time you open this tool. Alternatively, you can save
search results to any of a number of file formats
including DBF, XML, TXT, or the clipboard.

Backward Compatibility
Is A Great Feature
Doug Dodge
ddodge@utah-inter.net
What has become my perennial favorite feature
about Visual FoxPro (or even FoxPro for that
matter) is that when migrating between different
versions there has been relatively little that has
changed. Oh, sure, there have been changes and
even sometimes core VFP engine behavior has
changed, but it has always seemed to be on the
margins for me.
In our particular case we have an amalgam of code
that dates at times back almost twelve (12) years.

62 code-focus

Included is code that dates from the early FoxPro


2.0 DOS days. Its mixed in with code that uses
classes but is anything but object oriented. IOW, the
code was malformed at inception but it worked.
The absolutely amazing thing in my mind is that,
with one small exception Ill mention in a moment,
all of that code worked absolutely without a hitch in
VFP 8!
I never worry about whether or not FoxPro will be
able to do something. If it cant, it can be extended
or I can connect with some program, DLL or object
that can. I suppose in todays world, where consistency is thought to be dull and somehow of lesser
value, that this is the bane of a product that just
continues to deliver.
Now, there is a new feature in the Visual FoxPro
language set that I have had to deal with as a result
of some changes to the VFP 8 engine.
The VFP engine has been tweaked to be more
compliant with the ANSI-92 SQL standard. This
immediately affects all SQL code that is not
compliant. However, if youre not yet ready to
migrate or dont have time to line up your GROUP
BY and HAVING command with the fields youve
chosen (part of the engine change) all you need to
do is SET ENGINEBEHAVIOR 70 and youre
immediately VFP 7.0 compliant.
No harm, no foul but you now can be much more
confident that when you use the new SET
ENGINEBEHAVIOR 80 command that your data
result set will have less ambiguity. Oh,
SYS(3099,70) and SYS(3099,80) are new but
they are exactly the same as the SET ENGINEBEHAVIOR commands. So, one command in your
startup program and thats about it.
So, youll need to look at GROUP BY stuff and how
you manage it with the SYS(3099) (or SET
ENGINEBEHAVIOR) command. I guess I just like
the notion that my current code doesnt need a
whole lot of attention or effort. However, in VFP 8
the default behavior is the new setting, so if you
start getting SQL-related error messages you should
check this first.
This way you can take the time you need to learn
this new behavior and, if youre like me, in 4-5 years
you can change and incorporate this new feature.
Having said all of that, my Favorite Feature is the
consistency of the product (while it continues to
embrace the future). I think peace of mind as a
developer one of the nicest features to have. VFP 8
delivers.

From the VFP Team:

VFP 8 Tips and Tricks


Some of the early adopters of VFP 8 have contributed tips for
some of the new features of this exciting release. Check out their
ideas, then jump into the product and try some of the new stuff. Youll find that
there is much more than meets the eye, with hundreds of additions, changes, and
improvements.
used for queries against native VFP tables, as well.

Using Schemas with CursorAdapter

John Koziol
Test Engineer,
Visual FoxPro Team
Microsoft
The Tips and Tricks here are
just a few of the great ideas
weve seen from folks whove
had a chance to look at Visual
FoxPro 8.0. Wed love to see
more tips, samples, and utility
downloads for VFP 8.0 on the
various community sites soon.

Chin Bae
Objective Micro Technologies, Inc.
cbae@objectivemicro.com
ave you ever wanted to force a particular
column of a query result set to be a specific data
type? In prior versions of VFP, one of the few
tricks that you can use is to wrap a PADR() around a
character expression to force it to be certain length.
Also, you could add $0 to any numeric expression to
force a numeric column to be a currency data type.

However, there is no simple trick that allows you to


cast a numeric expression as
integer or force a character
expression to be a memo field.

Besides the ability to cast numeric values as any


other data type (integer, currency, double, etc.),
CursorSchema allows you to cast virtually any data
type as a character type. The example in Listing 1
demonstrates most of the data transformations that
are possible.

Several Ways to Start the Environment


Manager
Ryan Katri
ryan@cobsystem.com

Fast Facts

Although the Environment


Manager is normally accessed
through
the
Task
Pane
Manager, there are other ways
to use use it.

With the CursorAdapter class,


Here youll find an assortment of
you no longer need to use any
great ideas for working with VFP 8.
tricks. The CursorAdapter class
Early adopters spill the beans
To have the Environment
has a CursorSchema property
about some of the most interesting
Manager easily accessible
that allows you to specify the
aspects of the new features.
whether
the
Task
Pane
schema of the result set.
Manager is running or not, it
Although the CursorSchema
can be added to your Tools
was intended to be used against
menu. To do this, run the Environment Manager
remote data that might not have the exact data
application using a command-line option:
types as native VFP data types, this property can be

Listing 1: Chin Baes example for casting different data types with the CursorAdapter.
***test.prg
PUBLIC oCA, aFld[1]
CLEAR
OPEN DATABASE HOME(2) + data\testdata.dbc
oCA = CREATEOBJECT(CATest)
AFIELDS(aFld, oXC.Alias)
? aFld[1,1], aFld[1,2], aFld[1,3]
? aFld[2,1], aFld[2,2], aFld[2,3]
? aFld[3,1], aFld[3,2], aFld[3,3]
? aFld[4,1], aFld[4,2], aFld[4,3]
? aFld[5,1], aFld[5,2], aFld[5,3]
DEFINE CLASS CATest as CursorAdapter
DataSourceType = Native
Alias = crsTest
*!* Use CursorSchema to do the following:
*!* Widen cust_id from 6 characters to 20
*!* Cast cnt as integer
*!* Cast avg as currency
*!* Cast the original numeric type of qty as a character

64 code-focus

*!* Add a memo field called notes


CursorSchema = cust_id C(20), cnt I, avg Y, qty C(12), notes M
PROCEDURE Init
LOCAL cSQL
TEXT TO cSQL NOSHOW PRETEXT 7
SELECT order_id, COUNT(*) AS cnt,
AVG(unit_price) AS avg,
SUM(quantity) AS sum,
MIN(product_id) AS notes
FROM orditems GROUP BY 1
ENDTEXT
*!* Remove CRLFs
cSQL = STRTRAN(cSQL, CHR(13)+CHR(10), )
this.SelectCmd = cSQL
*!* Force the usage of the defined schema
this.CursorFill(.T.)
ENDPROC
ENDDEFINE
***end test.prg

DO (HOME() + "envmgr.app") with "-m"

You can also invoke specific environment


sets programmatically via command-line
options by simply passing the name of the
environment set. For example, if the set is
named Toolbox, you would issue the
following command:
DO (HOME() + "envmgr.app") with "Toolbox"

Although the environment set name is not


case-sensitive, the full name of the set must be
specified.
By default, the Environment Manager will
always display a message box when an environment is set. To run it quietly, which is
sometimes preferred when an environment is
set programmatically (such as via a project
hook), add a parameter of .T. to the
command-line:
DO (HOME() + "envmgr.app") with "Toolbox", .T.

the environment saving and seeking code to


a select statement to make the process
easier.
THE 8.0 WAY: Now that we can use a
filtered index tag for a candidate index, the
problem becomes trivial. Create a filtered
index on the membercard column and set
the index as a candidate index.
CREATE TABLE memberships (membercard I)
INDEX ON membercard TAG membercard FOR ;
membercard<>0 CANDIDATE
INSERT
INSERT
INSERT
INSERT
INSERT

INTO
INTO
INTO
INTO
INTO

memberships
memberships
memberships
memberships
memberships

VALUES
VALUES
VALUES
VALUES
VALUES

(0)
(14)
(0)
(0)
(14)

As you can see, you can insert as many empty


values into the table as you want, but if you
do supply a value, it must be unique.

Code Compilers
Publisher
EPS Software Corp., Publishing Division
13810 Champion Forest Dr., Suite 202
Houston, TX 77069 USA
Phone: 281-866-7444
Fax: 281-866-7466
Rod Paddock, Editor in Chief
Markus Egger, Co-Publisher
Rick Strahl, Co-Publisher
David Stevenson, Associate Publisher
Ellen Whitney, Managing Editor

Writers In This Issue


Chin Bae
Markus Egger
Cathi Gero
Doug Hennig
Claudio Lassala
Mark McCasland
Drew Speedie
Chuck Urwiler
Mike Yeager

Technical Reviewers
Markus Egger
David Stevenson

Filtered Index Tag Works


On a Candidate Index

Art & Layout

Mike Yaeger
Siriusware, Inc.
myaeger@siriusware.com

Production

SCENARIO: I have an application that


started life in Foxpro 2.0 that keeps track of
members of an organization. A requirement
of the system is that hand generated
membership numbers have to be unique in
the system to insure that the wrong member
isnt credited with some activity.

Doug Dodge
Garrett Fitzgerald
Tamar Granor
Ryan Katri
Paul Maskens
Barbara Peisch
Rick Strahl
Cindy Winegarden

Rod Paddock

King Laurin GmbH


Scoot Design
friedl.raffeiner@tin.it
Franz Wimmer
King Laurin GmbH
39057 St. Michael/ Eppan, Italy

Printing
Roto Longo Ag.

Advertising Sales
Tammy Ferguson

tammy@code-magazine.com
David Stevenson

david@code-magazine.com

THE 2.0 WAY: We used fairly complex


code in the VALID clause of the GET statements for this field to save the current
controlling index and record number, set
the controlling index tag to the membercard column, do a seek on the current
value of membercard, then and set the
index tag and record pointer back to their
original states. In the case of the user
editing an existing record, we also had to
make sure that we didnt find the record we
were working on by mistake. If we missed
a place in our app and didnt do this
checking, duplicate membercard numbers
could be assigned.

Erna Egger

erna@code-magazine.com

Circulation & Distribution


General Circulation: EPS Software Corp.
Newsstand: Ingram Periodicals, Inc.
John S. Treworgy, Circulation Consultant

Subscriptions
Subscribe online at
www.code-magazine.com
Subscription problems?
subscriptions@code-magazine.com

Online
www.code-magazine.com

THE 3.0 WAY: Primary and candidate


index tags were a new tool to prevent
duplicates at the database level in 3.0. If
every member had a number assigned, it
would have been easy to create a candidate
index on the membercard column to
prevent duplicates, but thats not how the
system worked. We did however, change

code-focus

65

Creating a Statusbar
Control with VFP 8
Rick Strahl

Visual FoxPro 8 offers many new features and opportunities to


make life easier. In this article Rick describes how to build a native VFP-based
status bar that fixes some of the problems found in the Windows Common Control OCX
version (MSCOMCTL.OCX) that ships with VFP and other development tools.
This article introduces several new VFP 8 features: Collections, the Empty object, AddProperty() and BindEvents(), and shows how to integrate these new features
into a useful component.

rstrahl@west-wind.com

Code for this article:


http://www.west-wind.com/
presentations/wwStatusBar/
wwStatusBar.zip

ne of the really cool features of VFP 8 is its


StatusBar often doesnt show up correctly either
ability to work with Windows Themes and
completely missing or missing the panels when the
provide fully themed user interfaces. Some
form first loads. It will show up correctly after the
people say that XP themes are nothing more than
form is resized for the first time. To work around this
fancy window dressing that suck up CPU cycles and
funky (and very inconsistent) behavior, you need to
screen real estate, but once you
insert several DoEvents and
start using themes its hard to
refresh the StatusBar from the
look back on the classic
Forms Activate event. And even
Fast Facts
Windows interface and not have
then it sometimes doesnt
it feel archaic.
behave correctly.
Visual FoxPro 8 offers full support

for themes and the XP style look.

Visual FoxPro 8 now supports


Now with VFP 8 supporting
Unfortunately the Windows
fully themed controls for all of
Windows Themes, the most
Common Control OCX that ships
its own native controls. Unfortuvisible problem is that the
with VFP 8 doesnt support this
nately, the same is not true of
StatusBar isnt themed. Figure 1
the Common Controls ActiveX
shows a VFP 8 application
same look. This article describes
controls (MSCOMCTL.OCX)
running under XP with the nice
how to build a status bar control
that many of us use to build
themed user interface, but a
that looks and behaves like an XP
enhanced user interfaces for our
status bar that is stuck in
style status bar, using several new
users. Even when using a ManiWindows Classic mode, which
features of VFP 8.
fest file (see sidebar), the various
looks funky and rather unprocontrols like the treeview,
fessional.
listview, statusbar, progressbar
and others do not inherit the Windows XP look and
Using a VFP based wwStatusBar class
feel and instead render in the classic style, which
looks a little bit funky when you run them inside an
To work around this problem, I decided to ditch the
otherwise themed application. To me this is most
ActiveX control and write a new VFP class that
noticeable with the StatusBar control, which gives
simulates a StatusBar using VFP code. The control
away a non-XP compliant application immediately.
can render in XP style, in classic style, and in a
Whats wrong with MSCOMCTL
modified classic style that mixes XP and classic styles.
It doesnt mimic all of the functionality of the
StatusBar?
ActiveX control, but implements most of the imporThe StatusBar ActiveX control has always had a
tant functionality in an easy to use container class.
number of problems. The most obvious is that the
Figure 2 shows a VFP application running with the
StatusBar does not properly show the sizing grip
wwStatusBar control in XP themes mode.
even when you enable the sizing grip in the control.
Well, it does sometimes. If you define the control in
code and add it to the form and run it in an MDI
form inside of the main VFP or another Fox applicaThe new Empty object and
tion window, then it works. But in a Top Level Form
the sizing grip never shows. Many of us have gotten
ADDPROPERTY command make it
around this by utilizing an image and embedding it
possible to dynamically build
on the statusbar (see Figure 1).

I use the StatusBar control in almost all of my applications and in many of them it has serious timing
problems with form rendering. The result is that the

66 code-focus

clean objects from scratch at


runtime.

Rick Strahl is president of West


Wind Technologies on Maui,
Hawaii. The company specializes in Web and distributed
application development and
tools with focus on Visual
FoxPro, .NET and Visual Studio.
Rick is author of West Wind
Web Connection, a powerful
and widely used Web application framework for Visual
FoxPro and West Wind HTML
Help Builder. Hes also a
Microsoft Most Valuable
Professional and a frequent
contributor to magazines and
books. He is co-publisher of
CoDe Magazine, and his book,
Internet Applications with
Visual FoxPro 6.0, is published
by Hentzenwerke Publishing.
For more information please
visit http://www.westwind.com/.

Listing 1: Adding panels in the wwStatusBar::Init method


*** Must call back to the default Init!
DODEFAULT()
loPanel = THIS.AddPanel("Ready",300,.T.,0)
loPanel.Icon = "bmp\webService.gif"
THIS.AddPanel("150 Topic(s)",135,.F.,2)

Heres how the end result works: The class is implemented as a VFP Container class, which builds a
panel collection and then dynamically renders the
collections content into various dynamically added
form controls.
To use the control, you first you drop the wwStatusBar control onto a form. The next step is to define
the panels, which you can add to the Init method of
either the form or the wwStatusBar class. Listing 1
uses the latter.

Figure 1: A nice themed VFP application with a StatusBar control thats stuck
in Windows Classic mode

The AddPanel method receives 4 parameters, the last


two of which are optional: The text, the width,
whether you want the width to stretch, and the text
alignment. The method then returns an instance of a
panel object which you can further customize. Note
that the third parameter the stretch value can
only be assigned to a single panel and causes that
panel to take up all the remaining space of the status
bar. In Figure 2, the first panel springs and resizes to
the width of the form while the second panel remains
a fixed size.
You can also configure the StatusBar overall by
setting the backcolor, font, and fontsize, which will
be passed down to the individual objects. In addition,
you can set the nStyle property to determine how the
bar renders (see Table 1).
To modify a panel, you have two options. You can
use the UpdatePanel method:
THISFORM.oStatus.Updatepanel(2,"New Text", ;
"bmp\classheader.gif")

which automatically updates the text and icon


(optional) based on the parameters passed. You can
also access the Panels collection directly. The Panels
collection consists of custom Panel objects which
contain the following properties: Text, Width, Spring,
Align (same as VFPs Alignment property), and Icon.
You can manipulate the panel like this:

Style Description
0

1
2
3

Automatic Automatically adjusts the


style depending on whether themes are
active
XP Style with Themes enabled
Classic Windows Style.
Modified Classic Windows Style.
Automatic uses this for classic Windows.

Table 1: Settings for the nStyle property.

Figure 2: A Themes enabled VFP application using the wwStatusBar control for
an XP compliant look.
loPanel = THISFORM.oStatus.Panels(2)
loPanel.Text = "New Text"
loPanel.Icon = "bmp\ClassMethod.gif"
loPanel.Width = 300
THISFORM.oStatus.RenderPanels()

If you dont modify the size of the Panel you can use
the RenderPanel(lnPanel) method which is more efficient. Anytime the size of a panel changes however,
the entire status bar must be redrawn by calling
RenderPanels().
By default the StatusBar can automatically resize
itself and stay anchored to the bottom of the form.
Note that at design time the status bar just sits
anywhere on the form, but at runtime the Resize()
method knows how to automatically resize the
StatusBar. If lAutoResize is .T. wwStatusBar uses
BindEvent() to hook the parent containers Resize
event and automatically resizes when the form is

code-focus

67

Windows portable executable


format supports a mechanism
called a Manifest file that, among
other things, works around DLL
Hell by allowing applications to
specify specific versions of DLLs
that the application is to use.
Manifests can contain version
numbers for specific files. You
can create a manifest file for any
EXE file simply by creating a file
named the same name as the exe
with a .manifest extension. So,
wwHelp.exe becomes
wwHelp.exe.manifest, for
example.
Manifests can be used to tell non
XP applications to use XP style
controls (if they follow certain
rules, that is) by specifying the
use of version 6.0 of the
Common Controls library. A
manifest file that does this looks
like this:
<?xml version="1.0" encoding="UTF-8"
standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="X86"
name="Microsoft.Winweb.
WebMonitor"
type="win32"
/>
<description>West Wind Web Monitor</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.
Common-Controls"
version="6.0.0.0"
processorArchitecture="X86"
publicKeyToken="6595b64144
ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>

This exact approach works and is


required for .Net applications to
be XP aware. VFP 8 is natively
XP aware, so it doesnt need this
tweak for native controls. Unfortunately, the tweak also doesnt
fix the problem with Common
Controls nor does it work for
VFP 7 applications, as VFP
controls are owner-drawn
controls rather than windowed
controls.

68 code-focus

Listing 2: The AddPanel method uses dynamic objects


to add to the collection
*** wwStatusBar::AddPanel
LPARAMETERS lcText, lnWidth, llSpring, lnAlign
IF ISNULL(THIS.Panels)
THIS.Panels = CREATEOBJECT("Collection")
ENDIF
IF EMPTY(lnAlign)
lnAlign = 0
ENDIF
loPanel = CREATEOBJECT("EMPTY")
ADDPROPERTY(loPanel,"Text",lcText)
ADDPROPERTY(loPanel,"Width",lnWidth)
ADDPROPERTY(loPanel,"Spring",llSpring)
ADDPROPERTY(loPanel,"Align",lnALign)
ADDPROPERTY(loPanel,"Icon","")
ADDPROPERTY(loPanel,"Left",0)
THIS.Panels.Add(loPanel)
RETURN loPanel

resized. You can override this auto resizing behavior


by setting the flag to .F. and manually calling wwStatusBar::Resize() when needed. This may be important
if your forms Resize() handles many different items
on a form and the order of the various resize operations is important.
Finally, you can implement PanelClick() events that
fire when you click on any of the panels. An event
will fire and pass the number of the panel, at which
point you can change the text or otherwise manipulate the panel.

How does it work?

Notice also the new Empty object which creates an


object with no properties or methods. Its similar to a
SCATTER NAME object we could create in VFP 7
from a table record, except now we can create it
directly and with no properties on it at all. The new
AddProperty() function then allows you to dynamically add properties to this object. AddProperty()
works on any object, but its specifically designed for
Empty and SCATTER NAME objects which dont
have AddProperty methods. To match, theres also a
RemoveProperty() function in VFP 8.
To display the panels in the container the RenderPanels method is called. RenderPanels() walks
through the Collection, figures out the total width of
the bar and then fits the panels into the available
space. RenderPanels() figures the size and walks
through the collection, but then delegates the actual
drawing to the RenderPanel() method (Listing 3).

The new BINDEVENT function


makes it possible to build components that can encapsulate parent
object manipulation right into the
component without requiring the
parent to take action.

Manifest Files

Theres nothing tricky about this method, which only


serves to dynamically throw controls on the
container and format and size them correctly and in
the correct order. First, a textbox is put down. Then
if an icon is specified, its created at the beginning of
the panel and overlaid and the text adjusted to skirt
the icon. Finally, a panel separator is laid down.

The wwStatusBar class is based on a container


control that hosts several controls that together simu-

When the form or container is resized, the status


bar should resize with it and the code that handles
this (Listing 4) resizes the statusbar to fit its parent
containers width and locate itself on the bottom
of it.

Figure 3: Three different modes are available for the wwStatusBar: 1 - XP Style, 2 - Classic Style and 3- Modified Classic.

You can manually call this Resize() method, but by


default the StatusBar automatically resizes itself
based on the Parent containers resize. I used the
new VFP 8 BINDEVENT() function in the Init() of
the control to tie the StatusBars Resize method to
the parent containers Resize:

late Statusbar operation. Its made of disabled, nonthemed, transparent textbox controls and a few
images that make up the sizing grips and separators,
which are placed onto the container in just the right
order. The new VFP Collection class is used to
manage the panels. The process begins with the
AddPanel method (Listing 2), which creates a new
object to add to the Panels Collection:
AddPanel adds a new item to the Panels Collection,
but it doesnt render anything yet. Collections make
life much easier when you are building lists like the
Panels here. In VFP 7.0 I might have used an array,
which is more work to size and then parse and retrieve
values from. With a collection, the process of adding
and retrieving items from the list is much easier.

IF this.lAutoResize
BINDEVENT(THIS.Parent,"Resize",THIS,"Resize")
ENDIF

This simple command tells VFP to monitor the


Resize event of the parent container and call the
Resize method on the wwStatusBar (THIS) if it fires.
BINDEVENT() is a very powerful tool to allow user
controls to handle events fired by parent controls,
and Resize is a good example of an event that can be
trapped and put to use in child controls without
having to write code in the parent container.

Listing 3: The RenderPanel method is the wwwStatusBar workhouse method


* wwStatusBar::RenderPanel
LPARAMETERS x, llFirstRender
LOCAL loLabel as TextBox

ELSE
loLabel.Width = lnWidth
ENDIF

*** Create the panel textbox


loPanel = This.Panels(x)
If Type("THIS.Panel" + Transform(x)) # "O"
This.AddObject("Panel" +
Transform(x),"TextBox")
loLabel = Evaluate("THIS.Panel" + Transform(x))
loLabel.Themes = .F.
loLabel.BackStyle = 0
loLabel.ReadOnly = .F.
loLabel.TabStop = .F.
loLabel.DisabledForeColor = this.Parent.ForeColor
loLabel.Enabled = .F.
loLabel.Height = this.Height - 2

*** Store Left value so we can handle clicks


loPanel.Left = loLabel.Left

IF this.nDisplayMode = 2
*** 3D Box no shadow must be closer to top
loLabel.Top = 1
loLabel.BorderStyle = 1
ELSE
loLabel.Top = 4
loLabel.BorderStyle = 0
ENDIF
ELSE
loLabel = Evaluate("THIS.Panel" + Transform(x))
Endif
*** Inherit Font
loLabel.FontName
loLabel.FontSize
loLabel.FontBold

from container
= This.FontName
= This.FontSize
= This.FontBold

loLabel.Value = loPanel.Text
IF llFirstRender
loLabel.Left = THIS.nRenderPosition
ENDIF
loLabel.Alignment = loPanel.Align
loLabel.Visible = .T.
lnWidth = loPanel.Width - 2
IF lnWidth < 1
loLabel.Width = 1

*** Draw Icon into textbox


If !Empty(loPanel.Icon)
If Type("THIS.PanelIcon" + Transform(x)) # "O"
This.AddObject("PanelIcon" +
Transform(x),"Image")
Endif
loIcon = Evaluate("THIS.PanelIcon" + Transform(x))
loIcon.Picture = loPanel.Icon
IF llFirstRender
loIcon.Left = THIS.nRenderPosition + 4
ENDIF
loIcon.Height = 16
loIcon.Width = 16
loIcon.Top = 5
loIcon.Visible = .T.
loLabel.Value = "
Endif

" + lolabel.value

THIS.nRenderPosition = THIS.nRenderPosition +
loLabel.Width + 1
*** Paint XP style separator after all but last
panel
If llFirstRender AND this.nDisplayMode # 2 AND ;
x < This.Panels.Count
If Type("THIS.PanelSep" + Transform(x)) # "O"
This.AddObject("PanelSep" +
Transform(x),"Image")
Endif
loImage = Evaluate("THIS.PanelSep" + Transform(x))
loImage.Left = THIS.nRenderPosition
loImage.Top = 5
THIS.nRenderPosition = THIS.nRenderPosition + 2
loImage.Picture = this.cXPSeparatorPicture
loImage.Visible = .T.
Endif

Collections
New to collections? Collections
are somewhat similar to arrays,
but in an objectified way. You
can use simple methods like
Add, Remove and Clear to
manipulate collections and,
unlike arrays, you dont have to
track the size of the structure
yourself. Collections also allow
access using indexes, which
can be either numeric or via key
name. For example, its much
nicer to reference an item as
loData.Columns(Company)
than using loData.Columns[5].
By specifying a key in the Add()
method you can greatly
simplify access to the collection
content. Collections lend themselves well for almost any listbased structure that needs to
be dynamically built and
accessed.

Listing 4: Resizing and positioning of the wwStatusBar is accomplished automatically with its Resize event.
LOCAL lnOldLockScreen
lnOldLockscreen = THISFORM.LockScreen
THISFORM.LockScreen = .T.
THIS.Left = 0
THIS.Width = THIS.Parent.Width
THIS.Top = THIS.Parent.Height - THIS.Height

IF VARTYPE(THIS.Panels) = "O"
THIS.RenderPanels()
ENDIF
THIS.oThumb.Left = this.Width - THIS.oThumb.Width
THIS.oThumb.Top = this.Height - THIS.oThumb.Height
THIS.oShadow.Width = THIS.Width
THISFORM.LockScreen = lnOldLockScreen

wwStatusBar can figure out automatically whether


its running under Themes or not and render the
appropriate view. To do so, it uses the new
SYS(2700) function which returns .T. if Themes are

active. The nStyle_Assign method deals with changes


to the nStyle value and internally delegates to an
nDisplayMode property that contains the real display
mode. It uses XP Style for Themes and the modified

code-focus

69

Listing 5: Handling the PanelClick by way of the MouseDown event


* wwStatusBar :: MouseDown
LPARAMETERS nButton, nShift, nXCoord, nYCoord
LOCAL x, loPanel
FOR x=1 TO this.Panels.Count
loPanel = this.Panels(x)
*** Calculate the offset and compare

Classic mode for classic apps, which displays the


thumb but doesnt use the block panels.

BINDEVENT is a new VFP 8


function that allows you to
hook events of objects and get
notified by way of a method
call. BINDEVENT takes 4 parameters: A source object and
event method, and a target
object and method handler. The
source is the object youre
listening for events from while
the handler object is the event
sink thats notified when the
event occurs. Heres how it
works: You set up BINDEVENT
in your startup code, which can
be anytime after the event firing
and handling objects are in
scope. You tell it which event
you want to listen for, and then
specify which method should
be called in response to this
event.
BINDEVENT is extremely useful
to allow black box components
to encapsulate processing logic
that would otherwise require
extra setup code in the calling
application code.
Firing order for BINDEVENT can
be important and BINDEVENT
includes an nFlags parameter
that allows you to select when
BINDEVENT is called in the
event firing sequence. The
default is before the event fires,
but options let you run your
code after the event fires and to
suppress event binding code if
events are directly called as
methods. If you use
BINDEVENT in a reusable
component, be sure either to
provide a way to specify when
the event is hooked or a way to
disable the event binding and
allowing the code to be manually called.

code-focus
70 www.code-magazine.com

wwStatusBar also implements PanelClick events,


which require a little more work. To do this, I originally figured I could just use the Click event on the
textboxes I used as panels, but Click() doesnt fire on
disabled controls (which I used to keep the controls
transparent and inactive). Instead, I had to use the
controls own MouseDown event which fires and
bubbles up from the text controls to the container as
shown in Listing 5.

look with standard color schemes, however, Transparent will likely work better, or you can explicitly
choose a color for your form that works with any
mode. I suggest you play with the different modes
and the wwStatusBar BackStyle property to see what
works best for you.
Finally, realize that wwStatusBar has a dependency
on the images that are used to render the sizing grips
and separators. Figure 4 shows the wwStatusBar
with the embedded invisible image controls that hold
the 3 required images. I chose to include them in the

This code simply looks through the Panels collection


and tries to find the panel that the click occurred in
based on the coordinates. If found, it passes on the
event to the PanelClick method. You can handle the
event by overriding the PanelClick method on the
control:
Figure 4: The Statusbar control in the class designer. Make
sure the three images are found and included.

LPARAMETERS lnPanel
WAIT WINDOW "Heres my panelclick " + ;
TRANSFORM(lnPanel) + CHR(13) + ;
THIS.Panels(lnPanel).Text

container to force these images to build into the


project so you dont have to ship external files. The
default path for these images is in a relative .\BMPS
folder of the build directory. Make sure you include
these images or youll end up with broken images.

Some limitations
Keep in mind that this is a minimalist implementation that isnt completely event enabled. If you
change certain properties of the wwStatusBar object,
make sure that you always call RenderPanel() or
RenderPanels() to refresh the status bar display properly. RenderPanels() is required anytime the sizes of
panels change.
In design mode the status bar displays as a gray
container and it doesnt automatically resize and
attach itself to the bottom of a form like the ActiveX
control does. The display mode for the control is set
to Opaque by default, which guarantees that the
sizing grips look proper regardless of color scheme or
theme chosen by the user. In fact, most apps I
checked (including IE) do not have the status bar
follow the Windows color scheme. To get the best

Its too bad we have a nice XP


style interface for our forms but
are stuck with statusbars, treeviews, and listviews that are
stuck in the classic era.

Hooking events with


BINDEVENT

against panels
IF loPanel.Left <= nXCoord AND loPanel.Left +
loPanel.Width >= nXCoord
THIS.PanelClick(x)
ENDIF
ENDFOR

Rick Strahl

Das könnte Ihnen auch gefallen