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 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 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
select CUSTOMERS.* from CUSTOMERS where
companyname like 'C%'
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). 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. UPDATE customers ;
SET CONTACTNAME=ccustomer.contactname ;
WHERE ;
CUSTOMERID=OLDVAL('customerid','ccustomer');
AND ;
CONTACTNAME=OLDVAL('contactname','ccustomer')
VFP was able to generate the WHERE clause by referencing the KeyFieldList property of the CursorAdapter as well as parts of the UpdateNameList property. It also takes into account which field was changed and adds in the necessary clauses to ensure that you don't attempt to update a record that has been changed by someone else. Note that this is because we left the WhereType property at its default of "key fields and any modified fields."
Handling Errors
Obviously, not everything will go as planned when trying to update data from the CursorAdapter. As you well know, TableUpdate can fail for a variety of reasons, such as an update conflict or a record lock. Do you have to do anything special with the CursorAdapter class to detect these problems? The answer is, "it depends."
Let's create a simple update problem by locking the record that the CursorAdapter is attempting to update. If the class designer is still open, close it. Then, instantiate the deTest class with the NewObject function, just as you did above, and call the OpenTables method. Browse the cursor so that you can see the data, but don't change anything yet.
|
OPEN DATABASE (HOME(2)+"Northwind\northwind.dbc")
USE customers
LOCATE FOR customerid = 'CACTU'
?RLOCK()
You should get a return value of .T. to show that the record is actually locked by this instance of VFP. 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. ?TABLEREVERT(.T.)
REPLACE contactname WITH 'client 1'
Now switch over to the second instance and issue the following commands: CLOSE DATABASES all
OPEN DATABASE (HOME(2) + "Northwind\northwind.dbc")
USE customers
LOCATE FOR customerid = 'CACTU'
REPLACE contactname WITH 'client 2'
BROWSE
Return to the first instance, and attempt to update the changes with TableUpdate: ?TABLEUPDATE()
In this case, TableUpdate incorrectly returns a .T., leading you to believe that the update was successful! However, it was not, and this can be proven by invoking the CursorRefresh() method of the CursorAdapter, as in the following code: ?TABLEREVERT(.T.)
?lo.caCustomer.CursorRefresh()
The CursorRefresh method tells the CursorAdapter to re-execute the SelectCmd and retrieve the latest data from the base table. Examination of the ContactName field shows that the value was never updated from the CursorAdapter! 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.
CursorAdapter with ODBC
|
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. 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. 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. --@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. 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.
Parameterization
As a side note, you have now seen one way to parameterize the commands for a CursorAdapter class. In essence, every event that occurs in the class has a set of Before and After methods, such as BeforeUpdate and AfterUpdate. However, there is no BeforeSelect or AfterSelectinstead, 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. 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.
|
--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. 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.
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. KeyFieldList = "CustomerID"
UpdatableFieldList = ;
"CompanyName, ContactName, ContactTitle, "+ ;
"Address, City, Country"
UpdateNameList = ;
"CustomerID Customers.CustomerID, " + ;
"CompanyName Customers.CompanyName, " + ;
"ContactName Customers.ContactName, "+;
"ContactTitle Customers.ContactTitle, " + ;
"Address Customers.Address, "+;
"City Customers.City, Country Customers.Country"
Tables = "Customers"
However, the RecordSet will be created with its default CursorLocation and CursorType properties. Without changing these properties, the RecordSet is initially read-only; therefore, you will need to modify the oRS_Access method as follows: FUNCTION oRS_Access() as ADODB.RecordSet
LOCAL loRS as ADODB.RecordSet
IF VARTYPE(this.oRS)<>"O" THEN
this.oRS = NULL
loRS = NEWOBJECT("ADODB.Recordset")
IF VARTYPE(loRS)="O" THEN
loRS.CursorType= 3 && adOpenStatic
loRS.CursorLocation = 3 && adUseClient
loRS.LockType= 3 && adLockOptimistic
this.oRS = loRS
ENDIF
ENDIF
RETURN this.oRS
ENDFUNC
With these additional settings for the RecordSet, the CursorAdapter can now handle automatic updates.
CursorAdapter with XML
Last, but not least, let's build a CursorAdapter that uses XML as its data source. This scenario is interesting, since an XML document doesn't normally act as a data source. Also, the CursorAdapter does not automatically build SQL Update, Insert or Delete statements when the data source is set to XML. Therefore, this type of CursorAdapter will require the most coding to retrieve and update data.
In this example, I will use the SQLXML feature of SQL Server 2000 to provide an XML document. Also, since SQLXML supports updating via XML, we'll take the time to write the necessary code to perform updates. This assumes that you have configured SQLXML to allow HTTP data access to the Northwind database, and that you are allowing updates to the database with UpdateGrams.
In my case, I have set up IIS to use a virtual directory called "nwind" for HTTP access. Therefore, all of my examples will contain URLs that reference
http://localhost/nwind
to access SQLXML via IIS. 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. FUNCTION INIT() as Boolean
LOCAL llRetVal, lcMsg, laErr[1]
this.SelectCmd = "this.GetXml()"
llRetVal = THIS.CursorFill()
IF NOT llRetVal THEN
AERROR(laErr)
lcMsg = "Cursor was not filled!"
IF NOT EMPTY(laErr[2]) THEN
lcMsg = lcMsg + CHR(13) + laErr[2]
ENDIF
MESSAGEBOX(lcMsg,16,"XMLCursorAdapter Test")
ENDIF
RETURN llRetVal
ENDFUNC
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.
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[1]
this.UpdateCmd = "this.UpdateXML()"
this.SelectCmd = "this.GetXML()"
** balance of code skipped...
Note that we could have placed the property settings for UpdateCmd and SelectCmd in the list of properties that precede the Init 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.
| © Copyright Component Developer Magazine and EPS Software Corp., 2009 |
| DevX is a division of Internet.com. © Copyright 2010 Internet.com. All Rights Reserved. Legal Notices |