he 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. With 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.
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 VFP cursor is all built into the CursorAdapter class.
You can probably tell already that the CursorAdapter is an effective replacement for the local view and remote view technology from earlier versions (Note: neither of these features has been removed from VFP 8). But in some cases, it also replaces SQL Pass Through, and also reduces the need to work directly with ADO and XML in your code.
One key advantage to the CursorAdapter is for situations where you need to connect to more than one data source from within the same application. For example, if your application retrieves most of its data from SQL Server, but also needs to work with a handful of XML documents, the CursorAdapter can be used in both cases to make all the data appear to your application as a set of VFP cursors.
Another example might be a situation where the data is currently stored in VFP tables, but future plans are to move to a database server, like SQL Server or Oracle. You would build a set of CursorAdapter classes first for VFP and then, if necessary, replace these classes with SQL Server equivalents when necessary.
But, since we must walk before we can run, let’s take a basic tour through the CursorAdapter class and its different incarnations. After that, it will be 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.
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.”
If you’re not familiar with the enhancements VFP 8 brings to the Data Environment, you might think that using a builder in the DE to create a CursorAdapter would only be useful within a Form, not a class. However, the DE has been enhanced in VFP 8 so it can be instantiated without the presence of a form!
Start by creating a new Data Environment class with the create class command. Be sure to select the Data Environment class in the “Based On” drop down. Name the class deTest and store it in a class library called Tests.vcx. Once the class appears in the class designer, right click on the Data Environment and select “Builder” from the drop down. This brings forward the Data Environment builder.
In the data source type drop down, note the available options. Since this first example will connect to native VFP tables, choose Native. Once selected, use the ellipsis button to choose the Northwind database (default location is c:program filesmicrosoft visual foxpro 8samples
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.
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.
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 CursorAdapter 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 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 “Auto-update” 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.
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).
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
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
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.
While the Data Environment has a few property changes and a method, we’re actually not interested 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.
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.
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.
When creating your own CursorAdapter classes from scratch, you may be tempted to leave out the 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 submits) 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.”
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
orthwind.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
orthwind.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:
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:
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
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:
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:
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.
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.
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 RaisError. This can be done with the following code in BeforeUpdate:
LPARAMETERS cFldState, lForce, nUpdateType, ; cUpdateInsertCmd, cDeleteCmd 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.
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 SelectCmd property; instead, it modifies what is passed to the data source, for as long as the object exists.
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.
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.
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:
--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)+"'"
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.
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.
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 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 SelectCmd. 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.
Updating with OLE DB
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:
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
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
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 this.SelectCmd = "this.GetXml()" llRetVal = THIS.CursorFill() IF NOT llRetVal THEN AERROR(laErr) lcMsg = "Cursor was not filled!" IF NOT EMPTY(laErr) THEN lcMsg = lcMsg + CHR(13) + laErr ENDIF MESSAGEBOX(lcMsg,16,"XMLCursorAdapter Test") ENDIF RETURN llRetVal ENDFUNC
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 invoke 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" FUNCTION INIT() as Boolean LOCAL llRetVal, lcMsg, laErr 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 XML-type CursorAdapter has no default update mechanism, 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’.
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.
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.