the cursor adapter
TRANSCRIPT
-
7/29/2019 The Cursor Adapter
1/22
THE CURSOR ADAPTER
The CursorAdapter class is one of the most impressive accomplishments of the VFP 8 developmentteam. It will change the way many developers relate to their various data sources. With theintroduction of the CursorAdapter class in VFP 8, the Fox team has finally made a significantchange in the way a VFP application accesses data, whether it is native or from a remote datasource. Additionally, the setup of CursorAdapter classes will be somewhat familiar to those who arewell-versed in the behavior of views and SPT, as well as the alternative data sources of using ADORecordSets or XML documents.
The CursorAdapter class is unique in that it is the first VFP base class to provide conversionbetween native VFP cursors and ODBC, ADO or XML data sources, all within a single class. Inother words, the ability to translate an ODBC data stream, an ADO RecordSet, or an XMLdocument 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 viewand remote view technology from earlier versions (Note: neither of these features has beenremoved from VFP 8). But in some cases, it also replaces SQL Pass Through, and also reduces theneed 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 thanone data source from within the same application. For example, if your application retrieves most ofits data from SQL Server, but also needs to work with a handful of XML documents, theCursorAdapter can be used in both cases to make all the data appear to your application as a set ofVFP cursors.
Another example might be a situation where the data is currently stored in VFP tables, but futureplans are to move to a database server, like SQL Server or Oracle. You would build a set ofCursorAdapter 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 classand its different incarnations. After that, it will be easier to devise a strategy for building data classesusing the CursorAdapter Class.
Your First CursorAdapter ClassLike any other class, the best way to learn how to use it is to see how one is created. To keep thecomplexity low for these first examples, let's start by accessing VFP native tables with aCursorAdapter 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, OLEDB data, and an XML document.
First, you have two ways to create a CursorAdapter. You can use the Data Environment builder oryou can build the class "by hand" through a program or the class designer. This example will usethe 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 thinkthat using a builder in the DE to create a CursorAdapter would only be useful within a Form, not aclass. However, the DE has been enhanced in VFP 8 so it can be instantiated without the presenceof a form!
Start by creating a new Data Environment class with the create class command. Be sure to select
-
7/29/2019 The Cursor Adapter
2/22
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 DataEnvironment builder.
In the data source type drop down, note the available options. Since this first example will connectto native VFP tables, choose Native. Once selected, use the ellipsis button to choose the Northwind
database (default location isc:\program files\microsoft visual foxpro
8\samples\northwind\northwind.dbc).
Next, click the Cursors page, which is initially empty. Under the list box, choose the New button tocreate a new CursorAdapter class with the CursorAdapter Builder. Initially, you will see theProperties page, providing options for choosing the name of the class and the alias of the cursorcreated 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 samedata source as the data environment. Note that you could have a different data source for theCursorAdapter, allowing you to mix data sources between different classes (such as using ODBCfor one class and XML for another).
To specify how the CursorAdapter will retrieve data from the source, use the Data Access page ofthe builder. Click the Build button to activate a command builder dialog, where you can select thefield(s) of interest for your cursor. For this example, select the Customers table from the Table dropdown list, and then select the "Customers.*" option in the list box below. Click the single right-facingarrow to move the selection, and then press OK. This will build the following select command foryou:
select CUSTOMERS.* from CUSTOMERS
If you wish to add filters, joins, or other clauses to the query, you can type them directly into this editbox. However, if you wish to parameterize the query, there are several options, covered later in this
article. For now, let's add a WHEREclause so that it looks like the following:
select CUSTOMERS.* from CUSTOMERS wherecompanyname like 'C%'
This will make it easy to tell the difference between the base table and the resultant cursor, sinceonly 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 andverify that the schema matches your expectations before proceeding. The data fetching propertiesat 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 moredetail later.
Near the bottom of the data access page is the buffer mode override setting, which allows you tooverride any associated form's buffer mode setting. Generally, you want to use the optimistic tablebuffering mode unless you have a specific reason to use the row buffering mode. Set this toOptimistic Table Buffering for this example.
Finally, the "Break on error" setting at the bottom of the page controls how errors are handled by theCursorAdapter class. The default setting specifies that the class will trap its own errors and allow
-
7/29/2019 The Cursor Adapter
3/22
you to capture them with the AERRORfunction. Enabling this setting will cause a VFP error to occurwhenever any problems occur within the CursorAdapter class. This means that you will need to usethe ON ERROR command or Error event of the class to trap unhandled exceptions. Generally, youwill want to leave this setting off so that your code can quietly handle any exceptions that mayoccur.
The final page (labeled "Auto Update") configures how changes are applied to the base table(s) ofthe 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, youmust 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 lateron in this article. To finalize your selections in the CursorAdapter builder, click the OK button toreturn 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 dataAt this point, you can try out the Data Environment to see if it retrieves the data specified by theselect 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 seeonly 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 objectinstance; therefore, if you destroy the object reference to the CursorAdapter class, you will also losethe cursor and its contents. This means that you'll have to ensure that any CursorAdapter objectvariables stay within scope for as long as you intend to access the associated cursor.
Modifying VFP DataNow, let's see if the cursor allows updates and posts them properly to the base table. Try thefollowing lines of code in the command window:
REPLACE contactname WITH 'My Name Here'?TABLEUPDATE()SELECT customersBROWSE
Once you browse the Customers alias, you see the base table and should be positioned on therecord that you modified. If you didn't move the record pointer before issuing the Replacestatement, the record with 'CACTU' as the customer ID was modified. Regardless of which recordyou modified, this proves that the CursorAdapter is updatable and that the updates are being sentproperly to the base table.
-
7/29/2019 The Cursor Adapter
4/22
Under the HoodLet's open the Data Environment class that you just tested to see what the builder did for us. This isnot just an exerciseit is a great way to learn how to properly configure a CursorAdapter classshould 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 notinterested 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 classwork.Table 1summarizes 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 inListing 1.
This is probably the most educational place to look after the builder is finished to see how theproperties 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 propertychanges 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 theUpdateNameList propertythis 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 thetable name in this listing. However, without this exact format, your updates will fail, but withouterrors. 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 replacementfor a Local View. If you have ever built a local view, you probably can see the similarities: a SQLSelect statement is constructed, you specify which fields you wish to be updatable, you specify thefield 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 themodifications.
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."
http://showsupportitem%28%27table1%27%29/http://showsupportitem%28%27table1%27%29/http://showsupportitem%28%27table1%27%29/http://showsupportitem%28%27listing1%27%29/http://showsupportitem%28%27listing1%27%29/http://showsupportitem%28%27listing1%27%29/http://showsupportitem%28%27listing1%27%29/http://showsupportitem%28%27table1%27%29/ -
7/29/2019 The Cursor Adapter
5/22
Handling ErrorsObviously, 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 theseproblems? 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 customersLOCATE 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 willsee the error message "Record is not locked." This means that you can handle such errors in thesame 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. Tosee this in action, issue the following commands in the current instance of VFP (where theCursorAdapter is being used):
?TABLEREVERT(.T.)REPLACE contactname WITH 'client 1'
Now switch over to the second instance and issue the following commands:
CLOSE DATABASES allOPEN 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
-
7/29/2019 The Cursor Adapter
6/22
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 valuewas 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 orAfterDelete 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, iflResult 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 THENERROR 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 SQLServer as the back end instead of VFP. We'll start with using the ODBC driver from VFP to accessthe Northwind database on SQL Server. Also, let's build this CursorAdapter "from scratch" so thatevery aspect of the class is visible.
-
7/29/2019 The Cursor Adapter
7/22
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 thisfashion, the DataSource property expects a valid connection handle, which can be created
through theSQLConnect orSQLConnectString 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 THENTHIS.DATASOURCE = lnConn
ELSE** unable to connect
ENDIFThe connection string assumes that you are using a trusted connection to SQL Server; if you areusing SQL Server security instead, add the "uid=" and "pwd=" strings to the connection string tospecify the username and password for the connection. In either case, the return value is theconnection 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 willcreate a new connection to the server, and never release it:
IF this.DataSource > 0 THENSQLDISCONNECT(this.DataSource)
ENDIF
With these changes, you have a functional CursorAdapter class that produces a non-updatablecursor. Still, it may be a good time to test the class, to ensure that it can be instantiated and that itretrieves data properly, before allowing it to be updatable. Test it with the following code:
-
7/29/2019 The Cursor Adapter
8/22
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 theTables, 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 inListing2.
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 ErrorsAs with the native CursorAdapter, most errors are trappable in the "traditional" waytest the return
value ofTableUpdate 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, butODBC 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 tothe following:
--@custID and @oldContact set by earlier code or parametersUPDATE customersSET ContactName = @newContactWHERE CustomerID = @custID
AND ContactName = @oldContact
IF @@ROWCOUNT = 0RAISERROR('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
http://showsupportitem%28%27listing2%27%29/http://showsupportitem%28%27listing2%27%29/http://showsupportitem%28%27listing2%27%29/http://showsupportitem%28%27listing2%27%29/http://showsupportitem%28%27listing2%27%29/http://showsupportitem%28%27listing2%27%29/ -
7/29/2019 The Cursor Adapter
9/22
(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 THENcUpdateInsertCmd = 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.
ParameterizationAs 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 ofBefore and Aftermethods,
such as BeforeUpdate and AfterUpdate. However, there is
no BeforeSelect orAfterSelectinstead, these are
called BeforeCursorFill andAfterCursorFill, 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 CursorSchemaproperty 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 ofBeforeUpdate) and is initially
populated with the current setting ofSelectCmd. 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.
-
7/29/2019 The Cursor Adapter
10/22
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 ofthis.cCompanyName (a user-defined property). And since it
doesn't modify the original value ofSelectCmd, 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 IIIf 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 featurestill works in the CursorAdapter. The following example code shows how you can use a parameterin 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 couldeven provide a set ofAccess/Assignmethods to ensure the assigned value meets certain criteria.
Using Stored ProceduresAbove, it was suggested that using stored procedures would be a good way to get around thelimitations of handling errors. With that in mind, let's explore the approach of using storedprocedures with an ODBC-based CursorAdapter so we can get a feel for how much work isinvolved in manually handling the updates for a CursorAdapter class.
Essentially, this section is all about replacing the automatic generation of Update, Insert, and Deletecommands with calls to stored procedures on the data source. This means that you'll be dealing
with theUpdateCmd, 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 VFPCREATE PROCEDURE UpdateCustomerContact (
@CustomerID nchar (5),@ContactName nvarchar (30),
-
7/29/2019 The Cursor Adapter
11/22
@oldContact nvarchar (30))
ASIF @CustomeriD IS NULLRAISERROR('CustomerID is a required parameter',16,1)
ELSEUPDATE Customers
SET ContactName = @contactNameWHERE CustomerID = @customerIDAND ContactName = @oldContact
To save space, this procedure is lacking the full error handling code that you would normallyinclude. Regardless, there is enough code here to illustrate how to perform an update with theCursorAdapter 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
forUpdateCmd, 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 ofTableUpdate, 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."
-
7/29/2019 The Cursor Adapter
12/22
CursorAdapter with OLE DBOur 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, andmay provide access to more types of data sources than ODBC. The CursorAdapter uses OLE DBby hooking into the objects of ADO, which is the standard COM wrapper around the OLE DBtechnology. VFP will automatically convert the ADO RecordSet into a VFP cursor for us, and willalso 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 onethrough code.
Start by creating a new program called caADO.prg, and add the following code:
PUBLIC goCAADO as CursorAdaptergoCAADO = CREATEOBJECT('caADO')BROWSE
DEFINE CLASS caADO AS CursorAdapteroConn = NULLoRS = NULLAlias = "cCustADO"DataSourceType = "ADO"SelectCmd = "SELECT " + ;"CustomerID, CompanyName, ContactName, "+;"ContactTitle, Address, City, Country "+;"FROM Customers WHERE Customerid LIKE 'C%'"
FUNCTION Init()This.DataSource = this.oRSThis.oRS.ActiveConnection = this.oConnThis.CursorFill()
ENDFUNCENDDEFINE
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 Commandobject, simply because that's where ADO has placed the parameters collection. Any parameters inyour query are automatically handled by objects in the parameters collection of the commandobject.
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 ofAccess methods to create the references to these
objects.Listing 3shows the code for theAccess 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 theconnection string and open the connection, because the CursorAdapter does not open theconnection for you. This is because the connection isn't a property of the CursorAdapter, butinstead 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 verymuch like the cursor you retrieved using ODBC, since it contains the data from the same source.
http://showsupportitem%28%27listing3%27%29/http://showsupportitem%28%27listing3%27%29/http://showsupportitem%28%27listing3%27%29/http://showsupportitem%28%27listing3%27%29/ -
7/29/2019 The Cursor Adapter
13/22
Updating with OLE DBWithout 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.RecordSetLOCAL loRS as ADODB.RecordSetIF VARTYPE(this.oRS)"O" THENthis.oRS = NULLloRS = NEWOBJECT("ADODB.Recordset")IF VARTYPE(loRS)="O" THENloRS.CursorType= 3 && adOpenStaticloRS.CursorLocation = 3 && adUseClientloRS.LockType= 3 && adLockOptimisticthis.oRS = loRS
ENDIF
ENDIFRETURN this.oRSENDFUNC
With these additional settings for the RecordSet, the CursorAdapter can now handle automatic
updates.
CursorAdapter with XMLLast, but not least, let's build a CursorAdapter that uses XML as its data source. This scenario isinteresting, 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 setto XML. Therefore, this type of CursorAdapter will require the most coding to retrieve and updatedata.
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 toperform 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.
-
7/29/2019 The Cursor Adapter
14/22
In my case, I have set up IIS to use a virtual directory called "nwind" for HTTP access. Therefore, allof 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 CursorAdapterSET MULTILOCKS ON && need for table bufferingoCAXML = CREATEOBJECT('xcXML')BROWSE NOWAITDEFINE CLASS xcXML AS CursorAdapterDataSourceType = "XML"Alias = "xmlCursor"UpdateCmdDataSourceType = "XML"InsertCmdDataSourceType = "XML"DeleteCmdDataSourceType = "XML"BufferModeOverride = 5*custom properties
oXMLHTTP = NULLoXMLDOM = NULLcServer = "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 DataBefore 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 BooleanLOCAL llRetVal, lcMsg, laErr[1]this.SelectCmd = "this.GetXml()"llRetVal = THIS.CursorFill()IF NOT llRetVal THENAERROR(laErr)lcMsg = "Cursor was not filled!"IF NOT EMPTY(laErr[2]) THENlcMsg = lcMsg + CHR(13) + laErr[2]
ENDIFMESSAGEBOX(lcMsg,16,"XMLCursorAdapter Test")
ENDIFRETURN 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.
-
7/29/2019 The Cursor Adapter
15/22
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
inListing 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 viaHTTP. 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 DataThe next step is to determine how to make this cursor updatable so that the changes can be postedback to our SQLXML server. SQLXML can take a special XML document, known as anUpdateGram, 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 theproperties 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, " + ;
http://showsupportitem%28%27listing4%27%29/http://showsupportitem%28%27listing4%27%29/http://showsupportitem%28%27listing4%27%29/http://showsupportitem%28%27listing4%27%29/ -
7/29/2019 The Cursor Adapter
16/22
"address customers.address, " + ;"city customers.city, country customers.country"
FUNCTION INIT() as BooleanLOCAL 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 forUpdateCmd and SelectCmd in the list ofproperties that precede the Init methodit 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 forUpdateXML (and oXMLDOM_Access), shown inListing 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, yourTableUpdate 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. IfTableUpdate succeeds, you should
be able to see your change on the server. However, ifTableUpdate 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, orDeleteCmd properties), you cannot see the contents of
the UpdateGram property.
Listing 6shows 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
http://showsupportitem%28%27listing5%27%29/http://showsupportitem%28%27listing5%27%29/http://showsupportitem%28%27listing5%27%29/http://showsupportitem%28%27listing6%27%29/http://showsupportitem%28%27listing6%27%29/http://showsupportitem%28%27listing6%27%29/http://showsupportitem%28%27listing5%27%29/ -
7/29/2019 The Cursor Adapter
17/22
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 forcross-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.
-
7/29/2019 The Cursor Adapter
18/22
Listing 1
The CursorAdapter Init() method generated by the builder
local llReturndo case
case not pemstatus(This, '__VFPSetup', 5)This.AddProperty('__VFPSetup', 0)
case This.__VFPSetup = 2This.__VFPSetup = 0return
endcasellReturn = dodefault()*** Setup code: DO NOT REMOVE***text to This.SelectCmd noshowselect CUSTOMERS.* from CUSTOMERS where CompanyName like 'C%'endtext******text to This.KeyFieldList noshowCUSTOMERIDendtext
******text to This.UpdateNameList noshowCUSTOMERID CUSTOMERS.CUSTOMERID, COMPANYNAMECUSTOMERS.COMPANYNAME, CONTACTNAME CUSTOMERS.CONTACTNAME,CONTACTTITLE CUSTOMERS.CONTACTTITLE, ADDRESS CUSTOMERS.ADDRESS,CITY CUSTOMERS.CITY, REGION CUSTOMERS.REGION, POSTALCODECUSTOMERS.POSTALCODE, COUNTRY CUSTOMERS.COUNTRY, PHONECUSTOMERS.PHONE, FAX CUSTOMERS.FAXendtext******text to This.UpdatableFieldList noshowCUSTOMERID, COMPANYNAME, CONTACTNAME, CONTACTTITLE, ADDRESS,CITY, REGION, POSTALCODE, COUNTRY, PHONE, FAX
endtext****** End of Setup code: DO NOT REMOVEif This.__VFPSetup = 1
This.__VFPSetup = 2endifreturn llReturn
-
7/29/2019 The Cursor Adapter
19/22
Listing 2.
The modified version of the Init() method
LOCAL lcConnStr, lnConn, llRetValWITH ThislcConnStr = "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, .AllowUpdateENDIF
ELSE** unable to connectllRetVal = .F.
ENDIFENDWITHRETURN llRetVal
-
7/29/2019 The Cursor Adapter
20/22
Listing 3.
The oConn_Access() and oRS_Access() methods
FUNCTION oConn_Access() as ADODB.ConnectionLOCAL loConn as ADODB.ConnectionIF VARTYPE(this.oConn)"O" THENthis.oConn = NULLloConn = NEWOBJECT("ADODB.Connection")IF VARTYPE(loConn)="O" THENloConn.ConnectionString ="Provider=SQLOLEDB.1;Integrated"+;"Security=SSPI;Persist Security Info=False;Initial
"+;"Catalog=Northwind;Data Source=(local)"
loConn.OPEN()this.oConn = loConn
ENDIFENDIFRETURN this.oConn
ENDFUNCFUNCTION oRS_Access() as ADODB.RecordSetLOCAL loRS as ADODB.RecordSetIF VARTYPE(this.oRS)"O" THENthis.oRS = NULLloRS = NEWOBJECT("ADODB.Recordset")IF VARTYPE(loRS)="O" THENthis.oRS = loRS
ENDIFENDIFRETURN this.oRS
ENDFUNC
-
7/29/2019 The Cursor Adapter
21/22
Listing 4.
The GetXML() Method
FUNCTION GetXml() as StringLOCAL loXMLHTTP as MSXML2.XMLHTTPloXMLHTTP = this.oXMLHTTP && access methodlcQuery = "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 GETloXMLHTTP.Send() && send itlcRetXML = loXMLHTTP.ResponseTextRETURN lcRetXML
ENDFUNCFUNCTION oXMLHTTP_Access() as MSXML2.XMLHTTPLOCAL loXMLHTTP as MSXML2.XMLHTTP
IF VARTYPE(this.oXMLHTTP) "O" THENthis.oXMLHTTP = NULLloXMLHTTP = NEWOBJECT("MSXML2.XMLHTTP")IF VARTYPE(loXMLHTTP) = "O" THENthis.oXMLHTTP = loXMLHTTP
ENDIFENDIFRETURN this.oXMLHTTP
ENDFUNC
-
7/29/2019 The Cursor Adapter
22/22
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.oXMLHTTPloXML = this.oXMLDOMIF loXML.loadXML(This.UpdateGram) THENloXMLHTTP.Open("POST", "http://" + this.cServer + ;"/" + this.cVDir, .F. )
loXMLHTTP.setRequestHeader("Content-type", "application/xml")
loXMLHTTP.send(loXML)lcRetVal = loXMLHTTP.responseTextloXML.loadXML(lcRetVal)loNode = loXML.selectSingleNode("root/pi('MSSQLError')")IF VARTYPE(loNode)="O" THEN*--we have an error from MSSQLlcErrMsg = loNode.TextlcAttrib = 'Description="'lnStart = AT(lcAttrib,lcErrMsg) + LEN(lcAttrib)lnEnd = RAT('"', lcErrMsg)lcErrMsg = SUBSTR(lcErrMsg, lnStart, lnEnd - lnStart)ERROR (lcErrMsg) && generate an error so TableUpdate fails
ENDIFENDIF
ENDPROC
FUNCTION oXMLDOM_Access() as MSXML2.DOMDocumentLOCAL loXMLDOM as MSXML2.DOMDocumentIF VARTYPE(this.oXMLDOM) "O" THENthis.oXMLDOM = NULLloXMLDOM = NEWOBJECT("MSXML2.DOMDocument")IF VARTYPE(loXMLDOM) = "O" THENthis.oXMLDOM = loXMLDOM
ENDIFENDIFRETURN this.oXMLDOM
ENDFUNC