Page 1


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

8 VFP 8: Visual FoxPro’s 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 doesn’t 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, I’m Eric Rudder, Microsoft Senior Vice President for the Developer & Platform Evangelism Division. I’d 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.

H

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. I’ve 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 company’s 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 Microsoft’s 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

7


VFP 8: Visual FoxPro’s 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. I’d like to include some details about Visual FoxPro 8.0, related announcements, and upcoming activities around the release.

T

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

9


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 “What’s New” section of the documentation will take you quite a while due to the large quantity of additional or changed features and commands. Let’s 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 “What’s 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.

N

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, that’s 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 can’t cover them all. useless as-is, because if it doesn’t 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.

Let’s 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 they’re 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 user’s 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 that’s 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 Let’s 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 you’ll 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 weren’t 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 you’ll find a new “DataEnvironment” option. Then, it’s 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 people’s 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, there’s 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 doesn’t matter here). In Figure 3, you see the label that’s 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 VFP’s 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.

VFP’s 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 Grid’s 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. You’ll 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. It’s 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 8’s 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 “What’s New in Visual FoxPro 8.0,” “The Hacker’s Guide to Visual FoxPro 7.0,” and “What’s New in Visual FoxPro 7.0”. He was the technical editor of “The Hacker’s 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 don’t support the FOR EACH syntax, which is especially awkward when they’re 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:

T

• 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 they’re 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 don’t 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 what’s 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 can’t 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 there’s 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 isn’t anything harder to pass than a member array. Since arrays must be


passed by reference using the @ operator, and you can’t 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. Wouldn’t 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 who’ve 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 developer’s point of view, there also isn’t 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, don’t 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, let’s examine what we had in the previous version.

F

Events Explained For Visual FoxPro developers, an event and the code that reacts to it are one and the same thing. But that’s actually incorrect. An event is a very different animal from the code that runs when an event occurs. Let’s 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 button’s 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.

Let’s 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 button’s “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. That’s not the case. When we add code to the Click() method, we are not really touching the button’s event at all. Instead, we simply put code into a method that goes with the button’s 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 isn’t really important. What’s important is that there is some naming convention that allows the compiler to automatically link our code to the event. Otherwise, we’d 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 that’s where VFP8’s new event binding comes in!

Manual Event Binding So let’s 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 form’s 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 doesn’t influence the original click event at all. You can check this by simply adding another message box to the button’s 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 doesn’t 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 let’s 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 you’ll 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 doesn’t 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 FoxPro’s 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 browser’s 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 isn’t used in all the software written in VFP. In the typical everyday project, developers simply don’t 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 (VFP’s 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 FoxPro’s 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.

W

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, let’s 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, we’ll 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 you’re 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, let’s 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 form’s 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, let’s 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. We’ll 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 you’re used to from prior versions of VFP while providing many new capabilities. Based on feedback we’ve 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


You’ll 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 you’ll 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, you’ll 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, let’s 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 didn’t 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.

Let’s 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, we’re 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 sheet’s 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. I’ll 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 don’t 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.” Let’s 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 don’t 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 you’ll 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 you’ve seen the basics, let’s move forward to see how things change when using SQL Server as the back end instead of VFP. We’ll start with using the ODBC driver from VFP to access the Northwind database on SQL Server. Also, let’s 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 we’re 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 didn’t 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 won’t be as effective for the ODBC CursorAdapter since we’re 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 can’t use _TALLY since it isn’t reliably correct for remote data. Instead, we can use SQL Server’s @@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, we’d 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 object’s 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 doesn’t modify the original value of SelectCmd, you don’t have to include any special coding to ensure that you don’t 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 you’ll 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, let’s 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, let’s 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 RecordSet’s 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, I’ll 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 we’ve 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, let’s 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 object’s UpdateCmd) cannot return a value. More accurately, if you return a value from the stored procedure, it doesn’t 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 don’t 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 that’s 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, we’ll 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 isn’t 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, let’s build a CursorAdapter that uses XML as its data source. This scenario is interesting, since an XML document doesn’t 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, we’ll 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. Let’s 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 we’ve 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, let’s 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 hasn’t 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 won’t 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 we’ve 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 CursorAdapter’s 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 object’s 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 you’re 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, we’ve covered a lot of ground, showing the four “faces” of the new CursorAdapter class. You’ve seen how to build the CursorAdapter through the DataEnvironment and CursorAdapter builders, through the visual class designer, and through a PRG. You’ve 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).

H

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 doesn’t 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, let’s 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 wouldn’t 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. Let’s 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 doesn’t deal with database tables, we probably don’t 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 isn’t 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 isn’t so easy, since the Error() event doesn’t 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

Let’s 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). I’m 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 that’s 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 we’ll 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 don’t care of it works or not since I can’t 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 .NET’s 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 doesn’t 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. Let’s 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 doesn’t 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 doesn’t 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, they’d have to be very quick, but hey, this is only an example!). In our little example, this wouldn’t 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 wasn’t 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 shouldn’t 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. What’s 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 let’s 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 let’s 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 wouldn’t have to worry about that possibility. The second Catch-block is not required and I just included it because I’d 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 that’s 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 doesn’t 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


What’s 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 doesn’t 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 let’s look at a few examples of mixed error handling and the effects it may have on your code. Let’s 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 isn’t 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. He’s 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 they’re 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.

I

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, I’ve 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 that’s so obvious, it tends to go in everyone’s defaults. With VFP3, it was Label.AutoSize = .T. VFP5 added ComboBox.BoundTo = .T. Now that we’re up to 8, I think we’ll 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 you’ve 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 won’t && 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 we’re 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 Gero’s .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 they’re 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

M

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 don’t 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 don’t 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 let’s 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. We’ll 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, let’s 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 FoxPro’s 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 FoxPro’s ability to interoperate with XML produced from and written to Windows .NET DataSets.

Listing 1: Sample .NET Code


â&#x20AC;&#x153;

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

â&#x20AC;&#x153;

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, What’s 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.

I

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.). I’ve 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 You’ll sense their enthusiasm as all you have to do is change the toolbar and the Component you read their suggestions for *DataSource and *DataSourceGallery. It’s 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 I’ll 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 I’m 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 doesn’t stop there. You can add oping a builder to do this for you is quite simple. third-party panes and create your own. I’ve 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 I’m most excited about of all the new VFP 8.0 features. Here are some of the ways I’m working on implementing it in the Visual MaxFrame Professional (VMP) framework. Create a handler for _Screen: You can’t 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. It’s 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. I’ve setup the extreme upper-right corner of my _Screen during development to fire up the Toolbox, as a poor man’s 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 I’m 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 doesn’t 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, it’s 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 isn’t a feature that’s receiving a lot of press, but I think it’s 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 couldn’t use it in the class designer. You then had to have code in your PageFrame’s 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 doesn’t 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 page’s Activate method. I don’t know about you, but most of the time, that’s the behavior I want, except I forget to add it to the page until I have a problem. Now, I’ll 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. I’m different, my favorite (and it’s 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! It’s 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. That’s it! It really is so simple to use. So simple that there’s 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 it’s put together and what you can do with it. Click on Environment Manager in the list on the left. The General tab isn’t 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”. You’ll 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 isn’t in the XSource.ZIP file that ships with VFP8, so


there’s 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 you’re 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 you’ve 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. It’s 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 I’ll 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 can’t, it can be extended or I can connect with some program, DLL or object that can. I suppose in today’s 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 you’re not yet ready to migrate or don’t have time to line up your GROUP BY and HAVING command with the fields you’ve chosen (part of the engine change) all you need to do is “SET ENGINEBEHAVIOR 70” and you’re 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 that’s about it. So, you’ll 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 doesn’t 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 you’re 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. You’ll 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 we’ve seen from folks who’ve had a chance to look at Visual FoxPro 8.0. We’d 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.

H

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 you’ll 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 Bae’s 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 isn’t 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 didn’t find the record we were working on by mistake. If we missed a place in our app and didn’t 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 that’s 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 doesn’t 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 it’s hard to refresh the StatusBar from the look back on the classic Form’s Activate event. And even Fast Facts Windows interface and not have then it sometimes doesn’t it feel archaic. behave correctly. Visual FoxPro 8 offers full support

O

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 doesn’t support this nately, the same is not true of StatusBar isn’t 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 What’s wrong with MSCOMCTL modified classic style that mixes XP and classic styles. It doesn’t 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. He’s 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)

Here’s how the end result works: The class is implemented as a VFP Container class, which builds a panel collection and then dynamically renders the collection’s 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 that’s 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 VFP’s 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 don’t 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 container’s 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 doesn’t need this tweak for native controls. Unfortunately, the tweak also doesn’t 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 form’s 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. It’s 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 it’s specifically designed for Empty and SCATTER NAME objects which don’t have AddProperty methods. To match, there’s 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

There’s 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, it’s 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 container’s 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 container’s resize. I used the new VFP 8 BINDEVENT() function in the Init() of the control to tie the StatusBar’s Resize method to the parent container’s Resize:

late Statusbar operation. It’s 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 doesn’t 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 don’t 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, it’s 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 it’s 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 doesn’t 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 you’re listening for events from while the handler object is the event sink that’s notified when the event occurs. Here’s 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() doesn’t fire on disabled controls (which I used to keep the controls transparent and inactive). Instead, I had to use the control’s 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 "Here’s my panelclick " + ; TRANSFORM(lnPanel) + CHR(13) + ; THIS.Panels(lnPanel).Text

container to force these images to build into the project so you don’t 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 you’ll end up with broken images.

Some limitations Keep in mind that this is a minimalist implementation that isn’t 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 doesn’t 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

It’s 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


CoDe Focus - Visual FoxPro 8.0  

CoDe Focus - Visual FoxPro 8.0

CoDe Focus - Visual FoxPro 8.0  

CoDe Focus - Visual FoxPro 8.0

Advertisement