the cursor adapter

Upload: ricardog1973

Post on 14-Apr-2018

241 views

Category:

Documents


3 download

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