2007/08/28 | [转] VFP 中的 CA
类别(数据库学习笔记) | 评论(0) | 阅读(1384) | 发表于 14:52
发表时间:2004-5-21 17:36:00    [回复]  [引用]
2 楼  
CursorAdapter 起步 之 三:可重用数据类

作者:Doug Hennig
译者:fbilo

VFP 的程序员们想要一个可重用的数据类已经很久了。尽管在过去的版本中也有许多解决这个问题的办法,不过总是有点美中不足。现在在 VFP 8里,我们有了真正的可重用数据类。这个月,Doug 为我们演示了怎样通过建立 CursorAdapter 和 DataEnvironment 的子类来建立可重用的数据类、以及怎样在表单和报表中使用它们。

正文
××

在过去的两期杂志中,我们讨论了在 VFP 8 中新增的 CursorAdapter 基础类。我个人的观点是,这是 VFP 8 中最重要的改动之一,因为它向我们提供了一个对象SQL Server这样的非VFP数据源的简单易用、统一的接口。此外,如你本月所能见到的那样,它们还形成了可重用数据类的基础。

在讲述可重用数据类之前,让我们先来看一下我建立的一些 CursorAdapter 和 DataEnvironment 的子类,我给它们增加了一些额外的功能,它们将成为我们的可重用数据类的起点。

SFCursorAdapter
***************

SFCursorAdapter (在附件 SFDataClasses.vcx 中) 是 CursorAdapter 的一个子类,它拥有一些额外增加的功能,如下:

※ 它可以自动处理参数化查询:你可以静态(一个常量)也可以动态(一个表达式,例如“=Thisform.txtName.value”,当 Cursor 被打开或者刷新的时候,这个表达式会被运算)的定义一个参数值。

※ 它可以在 Cursor 被打开以后自动在该 Cursor 上建立索引。

※ 对于 ADO,它还会执行一些特殊的工作,例如把 DataSource 属性设置为一个 ADO RecordSet,把这个 RecordSet 的 ActiveConnection 属性设置为一个 ADO Connection 对象,当用到一个参数化查询的时候,它还会建立一个 ADO Command 对象并把这个对象传递给 CursorFill 方法。

※ 它提供了简单的错误处理(cErrorMessage 属性里会有错误的信息)。

※ 它还有 CursorAdapter 中缺少的 Update 和 Release 方法。

这个类的 INIT 方法建立两个集合(使用新的 Collection 基础类,它是维护某些东西的集合用的),一个是为 SelectCmd 属性可能会用到的参数而准备的,另一个是用于在 Cursor 被打开以后应该自动建立的标记。它还会 SET MULTILOCK ON,因为这是 CursorAdapter Cursor 的需求。

This.oParameters = CreateObject('Collection')
This.oTags = CreateObject('Collection')
Set multilocks on

AddParameter 方法象 parameters 集合添加一个参数。给这个方法传递参数的名称(这个名称应该与该参数出现在 SelectCmd 属性中的那个名称相一致),根据需要也可以付上参数的值(如果你现在不给它传递参数的值,也可以以后再调用 Getparameter 方法来传递)。这段代码演示了一对 VFP 8 中的新功能:新的 empty 基础类,它没有任何属性、事件或者方法,因此是建立一个轻量级的对象的理想选择;还有 AddProperty() 函数,它的作用跟 AddProperty 方法类似,区别是它用于那些没有这个方法的对象。

lparameters tcName, tuvalue
local loParameter
loParameter = createobject('Empty')
addproperty(loParmeter, 'Name', tcName)
addproperty(loParmeter, 'value', tuvalue)
This.oParameters.Add(loParameter, tcName)

使用 GetParmeter 方法来返回一个特殊的 parameter 对象——通常是用在需要设置用于参数的值的时候。

lparameters tcName
local loParameter
loParameter = This.oParameters.Item(tcName)
return loParameter

SetConnection 方法用于将 DataSource 属性设置为希望的连接。如果 DataSourceType 是 “ODBC”,就给这个方法传递一个连接句柄;如果是“ADO”,DataSource 必须是一个ADO Recordset 对象,而且该对象的 ActiveConnection 属性必须要设置为一个活动 ADO Connection 对象,所以,我们需要向 SetConnection 方法传递这个 ADO Connection 对象, SetConnection 会建立一个 RecordSet,并且把这个 RecordSet 的 ActiveConnection 设置为被传递的 ADO Connection 对象。

lparameters tuConnection
with this
do case
  case .DataSourceType = 'ODBC'
   .DataSource = tuConnection
  case .DataSourceType = 'ADO'
   .DataSource = Createobject('ADODB.RecordSet')
   .DataSource.ActiveConnection = tuConnection
endcase
endwith

为了建立 Cursor,我们调用 GetData 方法而不是 CursorFill 方法,因为 GetData 能够自动处理参数和错误。如果你想要建立一个不带数据的 Cursor,那么就给 GetData 方法传递一个 .T.。这个方法建立的第一样东西,是与定义在 parameters 集合中的参数们同名的私有变量(在这里调用了 GetParametervalue 方法,该方法会返回参数对象的值,如果该对象的值是一个以“=”开头的表达式,那么返回的将是运算该表达式之后所获得的值。)下一步,如果我们是在使用 ADO 并且已经有了一些参数,这段代码会建立一个 ADO Command 对象,并把该对象的 ActiveConnection 属性设置为 Connection 对象,然后把这个 Connection 对象传递给 CursorFill 方法——这是 CursorAdapter 处理 ADO 参数化查询的需要。如果我们不是在用 ADO 、或者没有任何参数,那么代码会简单的调用 CursorFill 来填充 Cursor。注意,如果给 GetData 方法传递了 .T.,并且 CursorSchema 已经被填写好了,那么就是告诉 GetData 方法去使用 CursorSchema(这也是我想让 CursorAdapter 基类拥有的功能)。现在如果 Cursor 被建立起来了,代码会调用 GreateTags 方法来为该 Cursor 建立想要的索引;如果 Cursor 没有被建好,那么它会调用 HandleError 方法来处理任何发生了的错误。

lparameters tlNoData
local loParameter, ;
lcName, ;
luvalue, ;
llUseSchema, ;
loCommand, ;
llReturn
with This

* tlNoData参数指定是否要向 Cursor 中填充数据,如果要填充的话,
* 我们就要把建立存储所有参数的变量的活在这里就做掉而不是放到一个其它的方法中去。
* 因为我们希望这些变量的有效范围是私有的。

if not tlNoData
  for each loParameter in .oParameters
   lcName  = loParameter.Name
   luvalue = .GetParametervalue(loParameter)
   store luvalue to (lcName)
  next loParameter
endif not tlNoData

* 如果我们正在使用 ADO,并且有了一些参数,那么就需要有一个处理这些参数的 Command对象

llUseSchema = not empty(.CursorSchema)
if '?' $ .SelectCmd and (.DataSourceType = 'ADO' or (.UseDEDataSource and ;
  .Parent.DataSourceType = 'ADO'))
  loCommand = createobject('ADODB.Command')
  loCommand.ActiveConnection = iif(.UseDEDataSource, ;
   .Parent.DataSource.ActiveConnection, .DataSource.ActiveConnection)
  llReturn = .CursorFill(llUseSchema, tlNoData, .nOptions, loCommand)
else

* 填充这个 cursor.

  llReturn = .CursorFill(llUseSchema, tlNoData, .nOptions)
endif '?' $ .SelectCmd ...

* 如果 Cursor 建立成功,则为之定义所有的 Tag,否则则处理发生的错误。

if llReturn
  .CreateTags()
else
  .HandleError()
endif llReturn
endwith
return llReturn

还有一些方法这里我们就不说了,你可以自己去研究它们。HandleError 方法使用 Aerror() 来判断发生了什么错误,并把错误数组的第二个元素放到 cErrorMessage 属性中去。Requery 方法与 GetData 类似,不过它是用来刷新 Cursor 中的数据。调用这个方法而不是 CursorRefresh 方法的原因就象 GetData 一样:它能够处理参数和错误。Update 方法很简单:它就是调用 TableUpdate() 来提交当前数据源的更新,如果提交更新失败,则调用 HandleError 方法来处理错误。AddTag 用于在 Cursor 被建立后将你想要建立的索引的信息添加到 Tags 集合中,而 GetData 方法会调用的 CreateTags 方法则会在自己的 Index ON 语句中用到这个集合中的信息。

这里是使用这个类的一个例子,是从附件中的 TestCursorAdapter.prg 中拿来的。它从 SQL Server 自带的 Northwind 数据库的 Customers 表中取得数据。它的 SelectCmd 属性里是一个参数化查询的 Select 语句,向你演示了怎样用 AddParameter 方法来处理参数,以及怎样用 AddTag 方法来自动地为 Cursor 建立索引标识。

local loCursor as SFCursorAdapter of SFCursorAdapter, ;
loConnMgr as SFConnectionMgrODBC of SFRemote, ;
loParameter as Empty
lnHandle = sqlstringconnect('driver=SQL Server;server=(local);' + ;
'database=Northwind;uid=sa;pwd=;trusted_connection=no')
&& change password to appropriate value for your database
loCursor = newobject('SFCursorAdapter', 'SFDataClasses')
with loCursor
.DataSourceType = 'ODBC'
.Alias          = 'Customers'
.SelectCmd      = 'select * from customers where country = ?pcountry'
.SetConnection(lnHandle)
.AddParameter('pcountry', 'Brazil')
.AddTag('CustomerID', 'CustomerID')
.AddTag('Company',    'upper(CompanyName)')
.AddTag('Contact',    'upper(ContactName)')
if .GetData()
  messagebox('Brazilian customers in CustomerID order')
  set order to CustomerID
  go top
  browse
  messagebox('Brazilian customers in Contact order')
  set order to Contact
  go top
  browse
  messagebox('Canadian customers')
  loParameter = .GetParameter('pcountry')
  loParameter.value = 'Canada'
  .Requery()
  browse
else
  messagebox('Could not get the data. The error message was:' + ;
   chr(13) + chr(13) + .cErrorMessage)
endif .GetData()

* Now try to do an invalid SELECT statement.

.SelectCmd = 'select * from customersx'
if .GetData()
  browse
else
  messagebox('Could not get the data. The error message was:' + ;
   chr(13) + chr(13) + .cErrorMessage)
endif .GetData()
endwith
sqldisconnect(lnHandle)


SFDataEnvironment
*****************

在附件 SFDataClasses.vcx 中的这个数据和环境类要比 SFCursorAdapter 简单的多。但它增加了一些非常有用的功能:

×× GetData 方法会调用所有在这个数据环境类里面的 SFCursorAdapter 成员类的 GetData 方法,这样你就不需要自己去一个个的调用它们。与此类似的是,Requery 方法和 Update 方法也会调用每个 SFCursorAdapter 成员类的 Requery 和 Update 方法。

×× 象 SFCursorAdapter 一样,SetConnection 方法会把 DataSource 设置为一个 ADO Recordset,并把这个 Recordset 的 ActiveConnection 属性设置为一个 ADO Connection 对象。不过,它还会调用所有 UseDEDataSource 属性被设置为 .F. 的 SFCursorAdapter 成员类的 SetConnection 方法。

×× 它提供了一些简单的错误处理(cErrorMessage 属性会被填入错误信息)

×× 它有一个 Release 方法。

现在我们看看这个类的一对方法。GetData 非常简单:如果这个数据环境类的任何成员对象拥有 GetData 方法,则调用该方法:

lparameters tlNoData
local loCursor, ;
llReturn
for each loCursor in This.Objects
if pemstatus(loCursor, 'GetData', 5)
  llReturn = loCursor.GetData(tlNoData)
  if not llReturn
   This.cErrorMessage = loCursor.cErrorMessage
   exit
  endif not llReturn
endif pemstatus(loCursor, 'GetData', 5)
next loCursor
return llReturn

SetConnection 方法稍微复杂一点:如果它的任何成员对象有 SetConnection 方法、并且该成员对象的 UseDEDataSource 属性被设置为 .F.,则调用该成员对象的 SetConnection 方法;然后,如果有任何一个 CursorAdapter 对象的 UseDEDataSource 属性被设置为了 .T.,则使用类似于 SFCusrorAdapter 中的那样的代码来设置自己的 DataSource:

lparameters tuConnection
local llSetOurs, ;
loCursor, ;
llReturn
with This

* Call the SetConnection method of any CursorAdapter that isn't using our
* DataSource.

llSetOurs = .F.
for each loCursor in .Objects
  do case
   case upper(loCursor.BaseClass) <> 'CURSORADAPTER'
   case loCursor.UseDEDataSource
    llSetOurs = .T.
   case pemstatus(loCursor, 'SetConnection', 5)
    loCursor.SetConnection(tuConnection)
  endcase
next loCursor

* If we found any CursorAdapters that are using our DataSource, we'll need to
* set our own DataSource.

if llSetOurs
  do case
   case .DataSourceType = 'ODBC'
    .DataSource = tuConnection
   case .DataSourceType = 'ADO'
    .DataSource = createobject('ADODB.RecordSet')
    .DataSource.ActiveConnection = tuConnection
  endcase
endif llSetOurs
endwith

TestDE.prg 做了一个演示,它使用 SFDataEnvironment 作为容器,该容器中有一对 SFCursorAdapter 类。因为这个例子用的是 ADO,所以每个 SFCursorAdapter 都需要它自己的 DataSource,因此它们的 UseDEDataSource 属性都被设置成了 .F.(默认设置)。注意:只要简单的调用一下 SFDataEnvironment 的 SetConnection 方法就能搞定为每个 CursorAdapter 设置好 DataSource 属性的事情。

local loConn as ADODB.Connection
loConn = createobject('ADODB.Connection')
loConn.ConnectionString = 'provider=SQLOLEDB.1;data source=(local);' + ;
'database=Northwind;uid=sa;pwd='
&& change password to appropriate value for your database
loConn.Open()
set classlib to SFDataClasses
loDE = createobject('SFDataEnvironment')
with loDE
.AddObject('CustomersCursor', 'SFCursorAdapter')
with .CustomersCursor
  .Alias          = 'Customers'
  .SelectCmd      = 'select * from customers'
  .DataSourceType = 'ADO'
endwith
.AddObject('OrdersCursor', 'SFCursorAdapter')
with .OrdersCursor
  .Alias          = 'Orders'
  .SelectCmd      = 'select * from orders'
  .DataSourceType = 'ADO'
endwith
.SetConnection(loConn)
if .GetData()
  select Customers
  browse nowait
  select Orders
  browse
else
  messagebox('Could not get the data. The error message was:' + ;
   chr(13) + chr(13) + .cErrorMessage)
endif .GetData()
endwith
loConn.Close()

可重用数据类
************

现在我们已经有了可用的 CursorAdapter 和 DataEnvironment 的子类,让我们来谈谈可重用数据类的事情。

一件 VFP 程序员们已经向 Microsoft 要求了很久的事情是可重用数据类。例如,你可能有一个表单和一个报表,它们使用的是完全相同的一套数据,然而你却不得不重复的去手动向数据环境中添加一个个表或者视图——因为数据环境是不可重用的。某些程序员(以及几乎所有的应用程序框架提供商)采用了通过代码来建立可重用数据环境(那时候数据环境是不能可视化的建立子类的)的方法,并且在表单上使用一个 “loader”对象来建立 DataEnvironment 子类的实例。不管怎么说,这种方法总不是那么正规,并且无法用于报表。

现在,在 VFP8 里,我们不仅拥有了建立“能够向任何需要数据的对象提供来自任何数据源的数据”的可重用数据类的能力,还有了建立“能够寄宿数据类的”数据环境类的能力。在我写这篇文章的时候,你还不能在报表中使用 CursorAdapter 或者 DataEnvironment 的子类,不过你可以编程的添加 CursorAdapter 子类(例如把这些代码写在 DataEnvironment 的 INIT 方法中)来利用可重用类的优点。

让我们为 Northwind 数据库的 Customers 和 Orders 表建立一些数据类。CustomersCursor (在 NorthwindDataClasses.vcx 中)是 SFCursorAdapter 的一个子类,其属性如表1:

表 1. CustomersCursor 的属性

属性   值
Alias   Customers
CursorSchema CUSTOMERID C(5), COMPANYNAME C(40), CONTACTNAME C(30), CONTACTTITLE C(30), ADDRESS C(60),
    CITY C(15), REGION C(15), POSTALCODE C(10), COUNTRY C(15), PHONE C(24), FAX C(24)
KeyFieldList CUSTOMERID
SelectCmd  select * from customers
Tables   CUSTOMERS
UpdatableFieldList CUSTOMERID, COMPANYNAME, CONTACTNAME, CONTACTTITLE, ADDRESS, CITY, REGION, POSTALCODE, COUNTRY, PHONE, FAX
UpdateNameList CUSTOMERID CUSTOMERS.CUSTOMERID, COMPANYNAME CUSTOMERS.COMPANYNAME, CONTACTNAME CUSTOMERS.CONTACTNAME, CONTACTTITLE CUSTOMERS.CONTACTTITLE, ADDRESS CUSTOMERS.ADDRESS, CITY CUSTOMERS.CITY, REGION CUSTOMERS.REGION, POSTALCODE CUSTOMERS.POSTALCODE, COUNTRY CUSTOMERS.COUNTRY, PHONE CUSTOMERS.PHONE, FAX, CUSTOMERS.FAX

你不会以为我会是手动在属性窗口中输入所有这些属性的值吧?当然不是!我是用 CursorAdapter 的生成器来干的。这里的技巧是打开“Use connection settings in builder only(只使用在生成器中的连接设置)”,填入连接信息以获得一个活动连接,再填好 SelectCMD 以后,最后再用生成器来生成其它的属性设置。

现在,任何时候你需要 Northwind 的 Customers 表中的数据,只要简单的放一个 CustomersCursor 类就够了。当然,我们还没有定义任何连接信息,不过做到这样就已经很不错了,有了这个类就不需要担心怎么获得数据(ODBC、XML、ADO)、使用哪种数据引擎(比如 SQL Server、Access 以及 VFP 8中都有 Northwind 数据库)之类的事情了。

不过,要注意的是这个 Cursor 对付的是 Customers 表中所有的记录。可有时候,你又只想要一个客户的数据。那么,CustomerByIDCursor 是 CustomersCursor 的一个子类,它的 SelectCmd 已经被改成 “Select * from customers where customerid = ?pcustomerid”,还有下面增加的 INIT 方法的代码:

lparameters tcCustomerID
dodefault()
This.AddParmeter('pCustomerID', tcCustomerID)

这段代码会建立一个叫做 pCustomerID 的参数(它跟在 SelectCmd 中指定的是同一个),并且被设置成传递进来的任何值。如果没有值被传递进来的话,那么使用 GetParameter 方法来为这个参数返回一个对象,并在调用 GetData 之前设置它的 value 属性。

OrdersCursor 类类似于 CustomersCursor,只是它返回的是 Orders 表中的所有数据,而 OrdersForCustomerCursor 是它的一个子类,用于返回一个指定客户的所有订单。

要测试一下的话,请运行 TestCustomersCursor.prg,它会从 SQL Server 版本的 Northwind 数据库中 Customers 表的一个客户,然后做到 Access 版本的 Northwind 数据库所做的同样的事情。这个示例演示了怎样不为类指定连接信息,这个类自己就能灵活的完成任务,因此,它的可重用性是很强的。

示例:表单
**********

现在我们已经有了一些可重用类,让我们来用用它们。首先,我们来建立 SFDataEnvironment 的一个子类 CustomersAndOrdersDataEnvironment (哈哈,名字可有够长的,D.H牌冰糖葫芦!),它包含着一个 CustomerByIDCursor 类和一个 OrdersForCustomerCursor 类。由于我们希望在打开表之前设置连接信息,因此把它的 AutoOpenTables 属性设置为了 .F.,而且把前面两个 CursorAdapter 的 UseDEDataSource 属性都设置为了 .T.。现在,这个数据环境类已经可以被用来在某个表单中显示关于一个指定客户的信息以及他的订单了。

让我们来建立这么一个表单。附件中的 CustomerOrders.scx 表单的 DEClass 和 DEClassLibrary 属性已经被设置为了CustomersAndOrdersDataEnvironment 和 NorthwindDataClasses.vcx,这样就用上了我们的可重用数据环境类。这个表单的 Load 方法里面的代码有点多,不过这是因为它要支持 ADO、ODBC、以及 XML 数据源,并且它还要建立自己的连接,所以大多数代码都是处理这些问题的。如果它只支持一种数据源的话,比如只用 ODBC,再比如由另一个对象来管理连接(实际的应用程序开发中常常就是这样的),代码就会简单多了:

with This.CustomersAndOrdersDataEnvironment
  * 获得连接
    lnHandle = oApp.oConnectionMgr.GetConnectionHandle()
   .SetConnection(lnHandle)

  * 指定从 CustomerID 文本框中取得 cursor 参数的值
    loParameter       = ;
     .CustomerByIDCursor.GetParameter('pCustomerID')
   loParameter.value = '=Thisform.txtCustomerID.value'
   loParameter       = ;
     .OrdersForCustomerCursor.GetParameter('pCustomerID')
   loParameter.value = '=Thisform.txtCustomerID.value'

  * 建立一个空的 cursor,如果失败的话则显示错误信息

    if not .GetData(.T.)
    messagebox(.cErrorMessage)
     return .F.
   endif not .GetData(.T.)
endwith

这段代码用上了那两个 CursorAdapter 对象的 GetParameter 方法来把 pCustomerID 参数设置为表单上一个文本框中的值。注意在值里面用到的'=',它表示在你需要 value 属性的时候再去运算它的值,所以我们实际上有了一个动态的参数(这样就顺应了当用户在文本框中输入了新的值以后要将改动反应到参数中去的需要)。调用 GetData 方法是为了建立一个空的 Cursor,这样才能安顿那些数据绑定的控件。

txtCustomerID 文本框没有绑定什么数据。它的 Valid 方法先调用数据环境的 Requery 方法,然后再调用表单的 Refresh 方法。这样,当用户输入一个新的客户ID的时候,就能够 Requery 那个 Cursor,接着表单上其它控件也会被刷新。表单上的其它文本框被绑定到由 CustomersByIDCursor 对象建立的 Customers cursor 的字段中。那个 Grid 被绑定到由 OrdersForCustomerCursor 对象建立的 Orders Cursor。

运行这个表单,并输入一个 Customer ID 为“ALFKI”(见图1)。当你按下 Tab 键跳出这个文本框的时候,你会看到该客户的地址信息以及他的订单就出现了。试着改动一些这个客户的数据或者订单数据,然后关闭表单再打开,再输入一次“ALFKI”,你会看到你没做什么工作这些改动就都已经被写到后台数据库中了。


此主题相关图片如下:

图1、

酷吧,嗯?跟建立一个基于本地表或者视图的表单相比,并没多多少工作。更棒的是:试试把定义在 Load 方法中的 ccDATASOURCETYPE 常量改变为 “ADO”或者“XML”,然后这个表单无论是看起来还是实际工作起来都跟没改过之前一摸一样(如果你想用 XML,你需要象上个月的文章中所说的那样为 Northwind 数据库设置一个 SQLXML 虚拟目录,并把本月附件中的 XML 模板文件拷贝到那个目录里)。

示例:报表
**********

我们来试试报表。这里最大的问题是:与表单不同,我们既不能告诉报表去使用一个数据环境子类,也不能拖放一个 CursorAdapter 子类到数据环境中去。所以我们不得不向报表放入一些代码以将 CursorAdapter 添加到数据环境。尽管从逻辑上来看应该把这些代码放到报表数据环境的 BeforeOpernTables 事件中去,不过事实上这样做却是行不通的,因为——由于某些我不能理解的原因—— BeforeOpenTables 事件只会在你预览报表的每一页的时候才会触发。所以,我们只好把代码放在 Init 方法里。因为演示的需要,报表 CustomerOrders.frx 跟表单 CustomerOrders.scx 一样,要比实际开发的情况下会用到的代码更复杂一些。如果没有这些演示的需求的话,实际上可以简化到下面这样:

with This
   set safety off

  * 获得连接

    .DataSource = oApp.oConnectionMgr.GetConnectionHandle()

  * 建立客户和订单的 CursorAdapter 对象

    .NewObject('CustomersCursor', 'CustomersCursor', ;
     'NorthwindDataClasses')
   .CustomersCursor.AddTag('CustomerID', 'CustomerID')
   .CustomersCursor.UseDEDataSource = .T.
   .NewObject('OrdersCursor', 'OrdersCursor', ;
     'NorthwindDataClasses')
   .OrdersCursor.AddTag('CustomerID', 'CustomerID')
   .OrdersCursor.UseDEDataSource = .T.

  * 取得数据,如果失败,则显示错误信息

    if not .CustomersCursor.GetData()
     messagebox(.CustomersCursor.cErrorMessage)
     return .F.
   endif not .CustomersCursor.GetData()

   if not .OrdersCursor.GetData()
     messagebox(.OrdersCursor.cErrorMessage)
     return .F.
   endif not .OrdersCursor.GetData()

  * 从 Customers 设置一个与 Orders 的关系
    set relation to CustomerID into Customers
endwith

这里的代码比表单示例的要多一些,这是因为我们不能使用自定义的数据环境类导致不得不自己手动编码来代替。

现在,我们怎样才能尽可能简单的就把那些字段放到报表上去呢?由于 CursorAdapter 对象是我们用代码在运行时才添加到数据环境中去的,在设计时就没办法享受到拖放字段到报表上的方便了。这里有个小技巧:建立一个会建立这些 Cursor 的 PRG文件,并让这些 Cursor 处于有效范围内(可以采用挂起 PRG 的运行或者把 CursorAdapter 对象声明成 Public 的办法),然后使用快速报表功能来把那些字段放到报表上,这样报表控件的大小也设置好了。

图2展示了当你预览报表的时候该报表的情况。如果结合表单一起使用的话,你可以试试改动表单数据环境中的 #DEFINE 语句来换用其它类型的数据源。

总结
*****

    我们对新的 CursorAdapter 基础类的研究就到这里了。我个人对 CursorAdapter 的出现感到非常的兴奋,并计划给我的应用程序框架中的数据处理部件升升级以更充分的利用它的优点。

 

作者:huangdehua
专家分:5680
发表时间:2004-5-21 17:46:00    [回复]  [引用]
3 楼  
一个使用说明,关于cursoradapter类的

作者:四维

vfp8最激动人心的变化是  CursorAdapter 类,它为不同的数据源提供通用的数据接口。
下面介绍怎样用 CursorAdapter 改变在 VFP 8 中连接数据的方式,包括,native tables, ODBC, OLE DB, XML.  。   
CursorAdapter 类是 VFP 8 开发组的最给人印象深刻的成就之一。 它将会改变许多开发者连接各种不同的数据来源的方式。 开发小组在 VFP 存取数据的方式方面作了重要改变,将本地和远程数据连接方式进行了统一。另外,创建 CursorAdapter 类对那些已经熟练使用视图和SPT人来说并不费力。对使用ADO RecordSets 或XML可扩展标示语言文件的人也不费力。
CursorAdapter 类的独特之处在于,它是第一个提供本地游标、ODBC、ADO、XML数据源连接的基类,在一个类里面实现了所有连接功能。换句话说,要将ODBC数据源,ADO RecordSet 或XML可扩展标示语言文件翻译成一个 VFP 游标,CursorAdapter 类完全可以胜任。
你或许会说 CursorAdapter 是较早版本中本地视图和远程视图技术的替代 (注意: VFP 8中仍然保留这些功能). 但是在一些情形中,它也代替了SPT, 以及减少了使用ADO和XML时的代码量,可以直接使用ADO和XML。
CursorAdapter 的最大好处是,需要在同一个程序内连接到多个数据源的时候,它为你提供方便。举例说,如果你的程序大部分数据来自 SQL server,但是同时需要与XML可扩展标示语言连接,CursorAdapter 可以整合这两种情况,使程序取回的数据作为VFP的游标。
另外的一个例子是数据现在被储存在 VFP 表中 , 但是计划要移到一个数据库服务器 , 比如 SQL server或Oracle。 你需要先建立一组 VFP CursorAdapter 类,必要的时候以 SQL server以外的数据库代替这些类。
但是,就象我们能跑之前必须学会走一样,先概览一下 CursorAdapter 类和它的特性。 然后,使用 CursorAdapter 类来设计数据类是比较容易的。
建立第一个 CursorAdapter 类,象其他类一样,学习怎样使用它的最好办法是了解建立过程。第一次建立这些类的时候复杂程度低一些,我们开始用 CursorAdapter 类存取 VFP 本地的数据。 这很象使用本地视图取回 VFP 本地表的数据。 稍后在这一篇文章中,我们将会使用另一个 CursorAdapter 类连接到 SQL server数据库,ODBC和XML。
首先,你有二个方法建立 CursorAdapter 。 你能使用数据环境建立,也可以手动通过程序或类设计器来创建一个经过一个CursorAdapter类 。 这一个例子将会使用数据环境建立;较迟的例子将会手动建立.
如果你不熟悉 VFP 8 变化后的数据环境, 你可能认为在设计环境下创建的 CursorAdapter 只能在表单中使用,不能用于类。 然而,  VFP 8 中已经改善了设计环境,因此不需要在表单中就可以创建。
用creat class命令创建一个新的数据环境类。

此主题相关图片如下:

确定从下拉列表中选择based on数据环境类(dataenvironment) 。类名为Tests,所属类库名为tests.vcx.

此主题相关图片如下:

创建的类在类设计器中出现后,右键单击Data Environment ,选择builder,起动数据环境建立向导。

此主题相关图片如下:

在数据源类型项下,注意可选的选项。 因为第一个例子将会连接到本地的 VFP 表,选择native。 选择完以后, 使用‘省略号’按钮选择 Northwind 数据库(我这里是gzdata数据库)。 (默认位置是 c:\ program files\microsoft visual foxpro 8\ sample\northwind\ northwind.dbc)

此主题相关图片如下:

下一步,点cursors 页, 它初始值是空的。 在列表框中,选择new按钮用 CursorAdapter 设计器创建一个新的 CursorAdapter 类。

此主题相关图片如下:

首先,你应该看看Properties页,这里提供选项来选择类的名字和由类产生的游标的别名。
确定提供一个不同于表名字的别名,避免产生混乱。 在这里,使用 caCustomer 做类名, cCustomer 作为别名。 如果想让这个类用和数据环境一样的数据源,应该选择 "Use DataEnvironment data source" 。 注意你可以为 CursorAdapter 设置不同的数据源, 允许你在不同的类之间整合数据源.( 例如一个类使用ODBC数据源,另一个类使用XML数据源)

此主题相关图片如下:
要定义CursorAdapter 如何返回来自数据源的数据,使用设计器中的Data Access 页。 按build按钮激活一个对话框,可以选择游标包含的字段。

此主题相关图片如下:

在这个例子中,选择Customers表, 然后选择Customers.*。 点击向右的箭头移动选择项, 然后点ok。

此主题相关图片如下:

这为你建立下列SQL语句:
   select CUSTOMERS.* from CUSTOMERS
如果你想添加过滤器,连接, 或其他条件到查询,你可以在列表框中直接键入。 如果你想建立带参数的查询,有一些选项,在本文后面介绍。 现在, 让我们添加WHERE子句:
   select CUSTOMERS.* from CUSTOMERS where
   companyname like 'C%'
此主题相关图片如下:

这里可以看出源表和游标的不同,因为只有少数记录符合WHERE子句。
在第二个编辑框(schema)中已经为为你创建了字段列表。通常在继续以前花几分钟看看字段顺序是否符合你的习惯是有好处的
在这一页的下半部分有数据包设置对话框(data fetching),用来设置怎样处理远程数据包,当用vfp作为数据源的时候,这里的设置不发挥作用。(如上图)这里我们保留默认设置,稍后再讲述具体细节。在本页的底部附近是缓冲模式设定, 允许你设置任何被关联的表单的缓冲模式。有两个选项:开放式行缓冲和开放式表缓冲。

此主题相关图片如下:

通常,你使用开放的表缓冲模式,除非你有特殊要求使用行缓冲模式。在这个例子中设置为开放式的表缓冲。最后,“break on error”控制CursorAdapter类怎样来处理错误。默认设置是类(class)自行捕获错误,并且允许你用aerror()函数捕获这些错误。选定这个设置,CursorAdapter类内部不管发生什么错误,vfp都会出现错误信息。也就是说,你需要使用ON ERROR命令或者‘类’的ERROR事件来排除不需要报错的情况。通常情况下不选这项设置,以便程序能处理任何发生的例外情况。
最后一页 (auto update) 配置如何更新源表。在通常情况下,选择“自动更新(auto-update)”和“更新所有字段(update all fields)”。

此主题相关图片如下:
这将使cursoradaper类对游标(cursor)中数据的任何改变自动建立适当的更新、插入、删除机制。然而,你必须选择游标中的主关键字段,以便这些机制(更新、删除、插入)唯一的识别源表中的记录。在本例中,CustomerID是关键字段。因此,需要在其前面大上‘对号’。其他的设置暂时保留默认值。具体设置办法在本文后面讲述。设置完cursoradaper后,点击“ok”按钮,回到数据环境设置。此时,你应该在左边列表框中能看到caCustomer类, 在右边看到细节。如果你想更改这个类,你可以随时用数据环境‘builder’更改,选择需要更改的CursorAdapter 类,然后点击builder按钮。
存取 VFP 数据
此时,你可以测试数据环境,看看是否能取回在 CursorAdapter 的指令中筛选的数据。 使用命令窗户,例示 DE 类而且唤起 OpenTables 方法:
lo = NewObject("deTest","Tests.vcx")
   ? lo.OpenTables()
   BROWSE  
一个特殊情况是,CursorAdapter的游标连接到其它对象,如果你毁坏了指向CursorAdapter类的对象,会丢失游标和其中的记录。这就是说,你必须确保CursorAdapter对象参数在你打算存取的关联游标的范围内
编辑 VFP 数据
现在, 让我们看看是否能够更新游标,并且把更新准确地发送到源表。在命令窗口测试以下命令:
REPLACE contactname WITH 'My Name Here'
   ?TABLEUPDATE()
   SELECT customers
   BROWSE
浏览customers别名,你会发现修改过的记录已经更新到源表中。 如果你在发送replace命令之前没有移动记录指针,客户ID中 'CACTU' 所在的记录被修改。 不管你修改哪条记录, 这证明 CursorAdapter 是能够被更新,而且更新能够准确地被发送到源表。
让我们打开你刚刚测试的数据环境类,这不只是一个练习—它是一个很棒的方法学习当你决定在数据环境以外建立自己的类的时候,该如何正确地配置一个 CursorAdapter 类。
虽然数据环境有一些属性改变和一个方法, 我们实际上对那些改变不感兴趣。看一下下拉列表框中的属性列表,选择 caCustomer 类,看看建立 CursorAdapter 类的时候需要进行的设置。 表 1 概述被builder和每个 PEM 所做改变。
所有的属性包含在 "see Init" ,通过INIT方法中的代码来设置这些属性。 那一段代码显示在list 1中。
在builder设置完以后,看看一些属性是怎样设置的,这是最好的学习方法。你可以在这里或者通过builder改变设置值。然而,如果在这里改变设置值,你需要冒破坏一些功能的风险,因为你在这里改变的属性有可能在builder里不会发生改变。
不管怎样,你能在 Init() 代码中看到 SelectCmd 属性是如何叙述的, 同样在 KeyFieldList , UpdatableFieldList 和 UpdateNameList中也可以看到。 特别注意 UpdateNameList 属性—这个属性列出游标的每一个字段以及源表中对应的字段(带表名称)。
当从头创建你自己的 CursorAdapter 类的时候,你可能想在这个列表中省略表名字。 然而,如果不使用精确的格式,你的更新将会失败, 但是没有错误提示。 在后面讲不通过builder建立一个类的时候我将再说这一点。
前面我说过 CursorAdapter使用本地的数据源的时候 , 本质上是一个替代了本地视图。如果你曾经用过本地视图,你可以发现类似的地方: 生成一个SQL select语句,定义哪些字段将被更新,设置关键字段,剩下的事情让 VFP 来做。 一旦游标中取回数据,可以使用 TableUpdate() 发送更新到源表,而且 VFP 自动地建立必要的update,insert,delete语句。
还是以上面的例子, 取消cCustomer 别名中contact字段被替换的数据。通过TableUpdate , VFP 自动地产生 (并提交)下列更新命令尝试更新:
UPDATE customers ;
     SET CONTACTNAME=ccustomer.contactname ;
     WHERE ;
      CUSTOMERID=OLDVAL('customerid','ccustomer');
     AND ;
      CONTACTNAME=OLDVAL('contactname','ccustomer')
VFP 能够根据CursorAdapter的KeyFieldList属性和部分UpdateNameList属性中的值产生where子句。它通过子句记录那些已经被改变或添加的记录,保证你不会更新那些别人已经更新过的记录。注意,这是因为我们在它的WhereType 属性中保留默认值 "key fields and any modified fields."
在本文中出现table1,list1等,可惜我当时没下载,现在已经找不到了,不过不会对理解本文有什么影响,下次发“cursoradapter类的错误处理"
错误处理
明显的,当用 CursorAdapter 更新数据时候 ,并不能完全如你所愿。你知道,多种原因可以导致TableUpdate 更新失败, 例如更新冲突或记录加锁失败。你必须对 CursorAdapter 类做特别处理来发现这些问题吗? 答案是,"视情况而定 ."
我们来建立一个简单的更新问题:给CursorAdapter尝试更新的一条记录加锁。如果类设计器仍然是开着的,关掉它。然后, 像前面做过的一样,用 NewObject 函数启动detest类,而且启动 OpenTables 方法。浏览游标以便你能看到数据,但是先不要改变任何东西。
现在打开 VFP 8 的第二个实例,这样就能够加锁记录。 在命令窗口中运行下列语句加锁你将尝试更新的记录:
OPEN DATABASE (HOME(2)+"Northwind\northwind.dbc")
   USE customers
   LOCATE FOR customerid = 'CACTU'
   ?RLOCK()
你应该能返回.T.,表明记录确实被 VFP 的这一个实例加锁。
回到 VFP 的第一个实例,在命令窗口中运行下面的代码:
REPLACE contactname WITH 'updated'
   SET REPROCESS TO 2 SECONDS
   ?TABLEUPDATE()
在这种情况, TableUpdate 返回.F.,表现记录锁阻止更新成功。 如果你用 AERROR() 捕获错误而且显示错误结果, 你会看到错误信息 "记录没加锁"。 这意味着如果你对设置缓冲的表直接操作而不是一个游标,你能用同样的办法处理错误。
不幸地,不是所有的预期错误都会这样表现出来。需要特别注意的是更新冲突,比如一个使用者企图覆盖其他人所作的修改。想看看这种行为的结果,在当前的 VFP 实例中(CursorAdapter 正在使用)运行下面的代码:
?TABLEREVERT(.T.)
   REPLACE contactname WITH 'client 1'
现在转变在到第二个例证并且发行下列指令:
CLOSE DATABASES all
   OPEN DATABASE (HOME(2) + "Northwind\northwind.dbc")
   USE customers
   LOCATE FOR customerid = 'CACTU'
   REPLACE contactname WITH 'client 2'
   BROWSE
返回第一个实例, 尝试用 TableUpdate 发送更新:
?TABLEUPDATE()
在这种情况, TableUpdate 错误地返回.T.,使你相信更新成功了! 然而, 事实上并没有成功,而且这可以被CursorAdapter的CursorRefresh() 方法证明, 用下列代码:
?TABLEREVERT(.T.)
   ?lo.caCustomer.CursorRefresh()
CursorRefresh 方法告诉 CursorAdapter 再运行 SelectCmd 里的语句,并且取回来自源表的最新数据。 对ContactName 字段的测试说明 CursorAdapter 根本没更新字段值!
解决这一个问题的最简单的方法要利用 CursorAdapter 的 AfterUpdate 方法。这一个方法在 TableUpdate 发送保存每条记录的请求后被执行。注意这个‘方法’只对当前记录有效。 如果记录是新的,或者记录已经被删除,会激活AfterInsert 或 AfterDelete 方法。
AfterUpdate 方法捕获一些参数, 包括最初的字段状态,是否被强制更改, 和作为更新的命令。 最后一个参数 , lResult,是我们这部分主题最重要的,因为它告诉我们更新过程是否成功。
使用的另一个重要角色解决更新冲突问题的是系统变量 _tally, 它告诉我们最后一次操作影响了多少记录。 因此,如果 lResult 放回成功的, 但是 _tally是零,那么说明没有记录被更新,那么你可以判定这种情况是一个更新冲突。
简单说,解决这一个问题的简单方法是把下列代码加入 CursorAdapter 类的 AfterUpdate 方法:
LPARAMETERS cFldState, lForce, nUpdateType, cUpdateInsertCmd, cDeleteCmd, lResult
   IF lResult AND _TALLY = 0 THEN
     ERROR 1585 && update conflict
   ENDIF
有趣的是你在屏幕上看不到错误信息;错误信息被 TableUpdate “限制住了”,这就迫使你使用 AError 函数分析更新失败的原因。出现这样的现象是因为 BreakonError 属性保留了默认值 ,也就是错误不引起程序中断。如果你把这个属性设为“ture",那么"更新冲突" 错误信息就会出现,如果作了定义,你的ON ERROR句柄会被运行。
这一个更新冲突问题是为 CursorAdapter设计的,因为 VFP 8 没有自动发现这个问题方法。因此,当不用本地数据源的时候,这个代码( 或相似的代码)会用在你的CursorAdapter类设计中。
CursorAdapter with ODBC
现在你已经有了一定的基础, 让我们继续看看当后台数据库用 SQL server的时候怎样进行设置。我们将从使用 VFP 通过ODBC连接 SQL server上的 Northwind 数据库开始。同样, 让我们从头建立 CursorAdapter 以便可以看见类需要设置的各个方面。
首先,用下列指令在类设计器中建立一个新类:
CREATE CLASS caODBC OF tests as CursorAdapter
这里需要设置的最重要的属性是 DataSourceType 属性。 因为我们想通过ODBC连接 SQL server,将这一属性设置为ODBC。当使用这种连接方式的时候 , DataSource 属性需要有一个确切的连接句柄,可以用 SQLConnect 或 SQLConnectString 函数建立。 在任一情况,这些功能应该在 CursorAdapter 类的 Init 方法中使用下列代码:
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
连接字符串假定你使用可信赖的关联到 SQL server;如果你使用安全的连接到 SQL server,把 "uid"= 和 "pwd"= 串加入连接句柄指定使用者名称和密码。 任何一种情况下,返回的是连接柄 , 或错误发生时的否定值。 这一个连接柄被指定为 DataSource 属性以便 CursorAdapter 知道该到哪里声明。
下一个步骤是建立 SelectCmd 以便 CursorAdapter 知道需要从数据源取回什么数据。还有一个最好的放置地方是 Init 方法, 因为属性表限制字符串的长度。 把下列代码加入 Init 方法中,设定 DataSource 属性代码之后, 取回公司名字中以 "C" 开头的客户列表:
This.SelectCmd = "SELECT " + ;
     "CustomerID, CompanyName, ContactName, " + ;
     "Address, City, Region, Country " + ;
     "FROM Customers WHERE CompanyName LIKE 'C%'"
然后,你应该告诉 CursorAdapter 通过调用CursorFill 方法填充被关联的游标。 你可以省略这一个调用,并且手动从类之外调用, 或放在 Init 方法中,以便它能够自动填充游标。在Init 方法中设置 SelectCmd 之后,可以用 This.CursorFill()调用。
最后,你应该在类的Destroy方法中加上一些代码,以便在对象从内存中释放以后减少到服务器的连接。 没有这些代码的话,每一个新的实例将会产生一个到服务器的新连接, 并且不会释放它:
IF this.DataSource > 0 THEN
     SQLDISCONNECT(this.DataSource)
ENDIF
藉由这些变化,你拥有一个实用的 CursorAdapter 类,它能够生成一个只读的游标。在允许更新之前,测试一下类,确保可用,并能准确取回数据。用下列代码测试:
lo = NEWOBJECT("caODBC","tests")
   BROWSE
注意,你不需要像数据环境那样调用 OpenTables 方法。这是因为你把 CursorFill 方法直接加入了 Init 方法,使类在实例化之后自动地填充游标。
Updating ODBC Data(更新ODBC数据)
为了使这个类可更新,你必须正确地给Tables,  KeyFieldList , UpdatableFieldList 和 UpdateNameList 属性赋值。 并且设定 AllowInsert , AllowUpdate 和 AllowDelete 属性为true, 确定自动更新特征被激活。 再一次强调,最好的办法是在Init 方法中用代码设置这些属性。 Init 方法一些代码在list2 中。
在关闭类设计器之前,你可能想将 BufferModeOverride 属性换成 "5",“开放的表缓冲”以便移动记录指针的时候 , 自动更新不发生。
测试 CursorAdapter 的更新能力,把它作为一个实例,浏览游标,作一个更改, 然后提交 TableUpdate。 确定所作的更改被响应,调用 CursorAdapter 的 CursorRefresh 方法,再浏览一次。
处理ODBC错误(Handling ODBC Errors)象使用本地 CursorAdapter一样 ,大多数的错误处理是按照 "传统的"方法—测试 TableUpdate 返回的值,如果失败,使用 AError 来判断原因。 不幸的是,更新冲突的检测也是ODBC连接类型 CursorAdapter 的一个问题。
本地 CursorAdapter 错误的解决办法是在 AfterUpdate 方法中设置错误处理代码, 但这种方法对ODBC型 CursorAdapter 是无效的,因为更新失败的时候,我们不是要处理VFP错误,而是ODBC错误。 因此,最好答案是使用一个存储过程,或在更新句柄中增加一些代码发送到服务器。
回想一下,为本地的 CursorAdapter 更新错误的解决办法是检查 _TALLY,看看是否有记录被更新。为ODBC连接的解决办法与本地CursorAdapter是相似的,但是我们不能使用 _TALLY,因为在远程数据检测上它是不可靠的。 我们可以使用SQL server的@@Rowcount 系统函数来判定记录是否被更新。
如果你要写一个 T- SQL 批量更新语句来更新一笔记录,你应该象下面这样写:
--@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)
RaisError T- SQL 函数使 VFP 接收一个ODBC错误 (1526 号),  在第一个参数里写错误信息.(另外二个参数指出严重和错误的状态) 在这种情况下, 当@@Rowcount=0的时候RaisError 被调用, 表明早先的 T- SQL 语句没影响任何的记录。
所有讨论的这一些表明你可以使用 CursorAdapter 的 BeforeUpdate 方法来描述发送到服务器用来更新的语句。 BeforeUpdate 方法接受五个参数, 有趣的是最后的二个 (cUpdateInsertCmd 和 cDeleteCmd) 建立了关联(注:原文the last two (cUpdateInsertCmd and cDeleteCmd) are interesting in that they are passed by reference. )。 在它们被送去数据源之前 , 这允许你修改指令。
在我们的例子中,我们使用这一个方法增加一个对@@Rowcount的测试然后调用 RaisError 。把下列代码写入 BeforeUpdate :
LPARAMETERS cFldState, lForce, nUpdateType, ;   
     cUpdateInsertCmd, cDeleteCmd
   IF nUpdateType = 1 THEN
     cUpdateInsertCmd = cUpdateInsertCmd + ;
       " if @@ROWCOUNT = 0 "+ ;
       "RAISERROR('Update Failed due to update " + ;    
         "conflict.',16,1)"
   ENDIF
现在,为发送到后台数据库的每一行记录,用这段代码测试行是否被更新。 如果没更新, VFP 将会接收到错误,TableUpdate 将会失败,而且 AError 将会显示1526号错误,并显示预设的错误信息。
存在二个的问题。 首先,这是特别为 SQL server设置的;对其他的ODBC数据源 ( 比如ORACLE) ,这一段代码无效。 其次,这个错误信息是指一类错误,总是产生相同的 VFP 错误号, 而且在 VFP 中建立正确的错误处理句柄有一点困难。这一个问题可以通过在SQL SERVER服务器上建立通用的错误信息,每条信息对应着自己的唯一错误号码。
另外一个更好的解决方法是使用存储过程来执行更新,代替用VFP建立一个查询。 当然,采用存储过程的不利因素是不能使用VFP 的自动更新句柄来更新数据。
参数化设置   Parameterization
通过学习,你已经知道如何为 CursorAdapter 类的方法、属性添加命令。 基本上,在类中使用的每个事件(event)都有一组before和after方法, 比如 BeforeUpdate 和 AfterUpdate 。然而,没有 BeforeSelect 或 AfterSelect-替代它们叫做 BeforeCursorFill 和 AfterCursorFill,因为游标是用 SelectCmd 的结果来填充的。
BeforeCursorFill 方法接受三个参数, 而且返回 Boolean 值。 第一个参数 , lUseCursorSchema, 叙述 CursorSchema 属性是否控制游标的结构。 第二个参数 , lNoDataOnLoad,与视图的 NODATA 子句类似,只取回表结构,但是没有从数据源取回数据。
对于现在的讨论 , 第三参数 , cSelectCmd,是最有意思的。 我们已经提到过它 (象 BeforeUpdate 的 cUpdateInsertCmd 参数) ,先使用 SelectCmd 的当前设置。然而,如果你改变这一个参数的值,它不改变 SelectCmd 属性的值;取而代之的是,它的值改为被传给数据源的语句 。
举例来说, 假如你已经把 CursorAdapter 对象的 SelectCmd 设定为下列代码:
SELECT CustomerID, CompanyName, ContactName, City,
   Region, Country FROM Customers
在呼叫 CursorAdapter 的 CursorFill 方法之前, BeforeCursorFill 方法的 cSelectCmd 参数会包含这个语句。 假如你在这一个方法中用下列代码:
cSelectCmd = cSelectCmd + ;
     " WHERE CompanyName LIKE '" + ;
     this.cCompanyName + "%'"
这就导致实际的select命令总是包含如上面代码描述的where子句和 this.cCompanyName(自定义属性) 的当前值.因为它不改变 SelectCmd 的初始值, 你不必用任何特别的代码来确定在select命令中是否包含两个where子句。
参数设置第二部分
如果你以前用过视图,或 SQL pass through,那么你可能熟悉在参数前使用 "?" 字符。 这一个用法也适用于 CursorAdapter 。 下面的代码例子告诉你如何在 CursorAdapter 的 SelectCmd 属性中使用参数:
This.SelectCmd = "SELECT * FROM Customers " + ;
     " WHERE CompanyName like ?lcMyVar "
   lcMyVar = 'C%'
   This.CursorFill()
最要紧的是确定变量 "lcMyVar" 在 CursorFill 方法被请求之前被定义。否则的话,VFP会提示找不到值,使用者会无所适从。
你可以使用 CursorAdapter 的属性作为参数,用来替代本地的变量。这样做有这样的优点:只要对象存在,属性也会存在,而且你可以通过使用access/assign方法来确定分配的值符合规则。
使用存储过程
在上面说过, 使用储存过程是一个突破错误处理限制的好办法。让我们逐步探究在ODBC CursorAdapter中使用存储过程的方法。这样我们就能感觉出手动处理 CursorAdapter 类的更新错误是多么复杂。
这一段是关于通过调用数据源的存储过程来代替自动执行的update、insert和delete命令。 这意味着你必须处理 UpdateCmd , InsertCmd 和 DeleteCmd 属性, 而且假设 SQL server上的 Northwind 数据库已经建立存储过程来提供这些功能。  
例子, 让我们看一看一个简单存储过程的完整代码,你可以用它更新 Northwind 数据库的customer表的 ContactName 字段:
--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
为了节约空间,这一个存储过程没有包含全部的错误处理代码。 不管这个,已经有足够的代码来说明怎样在 CursorAdapter 类中执行更新。
幸运地, 建立 UpdateCustomerContact 存储过程作为更新命令,可以取代 BeforeUpdate 方法,用下面的代码:
LPARAMETERS cFldState, lForce, nUpdateType, ;
     cUpdateInsertCmd, cDeleteCmd
   cUpdateInsertCmd = ;
     "EXECUTE UpdateCustomerContact '" + ;
     EVALUATE(this.Alias+".CustomerID") + "','" +;
     ALLTRIM(EVALUATE(this.Alias+'.ContactName'))+ ;
       "','" + ;
     OLDVAL('contactname',this.Alias)+"'"
这里,代码放在 cUpdateInsertCmd 参数里,覆盖了默认的update命令。我使用了evaluate函数,是为了cursor的名字是动态的,说明cursor的名字可以很容易被改变,但是代码不变。还有,我使用了OLDVAL 函数取回 ContactName 字段被修改前的值。如果在存储过程的where子句里需要旧的数据,那么这个函数是必需的。这有点像自动产生update的情况。
记住,在记录被实际更新之前 ,通过TableUpdate自动呼叫 BeforeUpdate 方法。 因此, 无论UpdateCmd当前的值是什么,这个方法程序(指UpdateCmd)被禁用,而且总是使用存储过程。
注意,你也可以使用已经讨论过的参数设置方法,使用 BeforeUpdate 方法。 这就要求你为 CursorAdapter 提供 UpdateCmd设置,但是, 不要在参数里用固定的代码,要用变量或者属性,并在它们前面加上“?”。
在这里需要注意的重要的一点是 cUpdateInsertCmd( 或对象的 UpdateCmd属性) 不能返回任何值。 进一步说,如果你从存储过程返回一个值,它就没有地方 "去" ,那么这个值就会永远丢失。因此,在存储过程中添加一个RaisError呼叫,以便在更新过程中发生任何错误( 例如不正确的参数或一个更新冲突) 的时候,你的代码能够作出反应,这样做是非常重要的。你可以通过测试 TableUpdate 返回值来捕获错误 ,或者用 AError 方法, 然后分析错误。
相似的代码也应该写进 BeforeInsert 和 BeforeDelete 方法,以便它们也调用存储过程,而不是调用设置好的“查询”。 为了节约空间,我将留下代码当做 "读者的练习."
CursorAdapter with OLE DB
我们的下一个任务是看看如何通过 CursorAdapter 类使用OLE DB(对象连接与嵌入), 并且把它同我们用过的native和ODBC作一比较。OLE DB技术比ODBC有更多的处理能力, 而且能够连接的数据源类型比ODBC多。 CursorAdapter 通过嵌入ADO对象使用OLE DB,这是OLE DB技术标准的 COM 封装。 VFP 会自动地把ADO记录集  转换成一个 VFP 游标, 以及处理更新, 正如在早先的例子中讲到的一样。
第一件要做的事,当然还是建立一个新的 CursorAdapter 类,我们用代码建立一个。
开始建立一个新的程序,取名字 caADO.prg, 把下列代码添加进去:
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
在这段代码中,我们将 DataSourceType 设为ADO而且把我们对Customers的查询加入 SelectCmd 。 当 DataSourceType 是ADO的时候, DataSource 属性必须包含有效的 RecordSet 或命令对象, 这依赖于你如何使用 CursorAdapter。 如果你不使用参数化查询 (就象前面例子中讲到的用 "?")那么你可以用记录集( RecordSet );否则,你必须使用命令对象,因为ADO已经代替了参数选择。 你的查询中的任何参数在命令对象中自动地被处理。
在这种情况下,我们使用 RecordSet 对象 , 但是应该注意我们必须提供一个“连接”对象。 在这两种情形中,我使用Access方法建立了关于这些对象的参考。 list3 中是Access方法的代码。
两个Access方法首先检查对象是否已经被建立。 如果没有,那么就继续创建对象。在使用 RecordSet 的情形,你只需要建立对象,剩下的由 CursorAdapter 来做。 用“连接”对象的情况下, 你必须提供连接字符串(连接句柄)并且打开连接,因为CursorAdapter 不为你打开连接。这是因为“连接”不是 CursorAdapter 的一个属性, 而是 RecordSet 对象的一个属性。
下一次发:用OLE DB更新
用OLE  DB更新
如果不另外设定几个属性,这么简单的 CursorAdapter 是没有更新能力的。 把下面的代码放在定义类(define)的代码中,在init方法之前运行,将会允许自动更新:
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"
然而, RecordSet 会建立它的默认的 CursorLocation 和 CursorType 属性。 如果不改变这些属性设置, RecordSet 最初是只读的,因此,你需要按以下方法修正 oRS_Access 方法:
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
为 RecordSet 加上这些代码以后, CursorAdapter 就能处理自动更新了。
下一次发cursoradapter with XML
CursorAdapter with XML
最后, 让我们建立使用XML作为数据源的 CursorAdapter。 这一节很有趣,因为XML文本不像通常的数据源。 同样,当数据源设置为XML的时候, CursorAdapter 不会自动建立 SQL 更新, 插入或删除命令。因此,这种类型的 CursorAdapter 需要大量的代码接收和更新数据。
在这一个例子中,我将使用 SQL server 2000 的 SQLXML 提供XML文本。 同时,因为SQLXML支持通过XML更新,我们将花一点时间写必要的代码来执行更新。 假设你已经配置了SQLXML,允许HTTP数据存取Northwind 数据库,而且允许用 UpdateGrams 对数据库进行更新。
在这里,我在 IIS 上面设置使用一个被称为 "nwind" 的虚拟目录存放 HTTP 数据。 因此,我的全部例子将会包含这个网址:
http://localhost/nwind,经由%20IIS%20存取%20SQLXML。
让我们开始建立一个新的程序,程序名字为caXML.prg,用下列代码(创建一个类):
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
不同于通常的 DataSourceType 和Alias(别名)属性设置,这是我们第一次看见 xxxCmdDataSourceType 属性。 因为这是一个XML类型的 CursorAdapter,如果你想让它用来更新源数据,这些属性就必须进行设置 。推荐在这个类中使用 oXMLHTTP 和 oXMLDOM 属性,将会在下面做详细说明。
接收XML数据
在考虑 CursorAdapter 的更新能力之前,我们先研究怎样从SQLXML 服务器接收文本。 首先,一个简单的select命令不能工作,我们必须建立通用的 SelectCmd 。 这在 Init 方法中很容易做到,同时,在INIT中调用 CursorFill 方法,如以下代码:
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  
这一段代码建立 SelectCmd 作为本地用的方法,代替 SQL 命令。 以前的例子中没有这样做过,对任何 CursorAdapter 类,不管什么类型,都是合法的。然而,当你使用一个本地的方法作为 SelectCmd 的时候,你必须也提供代码用来更新,插入和删除, 因为如果不是一条SQL语句的话, VFP 是不会自动处理的。
当我们在 Init() 中使用 CursorFill 的时候,GetXML 方法被调用。 数据源设定为XML的时候,GetXML 方法必须返回只包含一个表的最终文本。 如果它包含多个表,你会得到料想不到的结果。 GetXML 方法在list4 中列示。
GetXML开始于提交一个 MSXML2.XMLHTTP COM 对象。 这个对象处理所有 HTTP 通信,包括发送请求到服务器并且返回结果。 你可以看到,oXMLHTTP 实例对象是被设计好的Access方法控制的,这样设计是为了防止连续地创建和破坏 COM 服务器。
然后,你可以看到我们的典型的SELECT描述, 除了LIKE子句有点稍稍不同以外。 HTTP 需要我们当十六位百分比字符显示到达25%的时候退出显示(指进度显示), 在 SQL server接收查询之前 , 这个值将会变成单一的百分比符号。
然后,代码用具体的查询要求来设置URL,并且通过HTTP发送URL到SQL SERVER服务器。SQL SERVER接收到这个查询,并且处理它,以XML方式返回值。这是因为我们已经在“查询”中包含了FOR XML子句。在这个例子中,XML的根元素叫做 "results" 。你从查询字符串中可以看出。这是按照你的习惯配置的。
此时, lcRetXML 包含一个来自 SQL server的XML数据流。 既然 GetXML 方法被 VFP 作为 SelectCmd 调用,你可以从 GetXML 方法中返回这个变量的内容,而且 VFP 将会把数据流转换成一个 VFP 游标。 你可以通过执行 caXML 程序测试。 会出现一个包含返回的XML文本内容的浏览窗户。 使用调试工具一步一步查看  GetXML 方法,可以看到在被转换到一个 VFP 游标并释放之前 , lcRetXML 变量的值是XML文本。
更新XML数据
下一个步骤是决定该如何使这个游标可更新,以便所作的更改能被发送回 SQLXML 服务器。 SQLXML 能使用一个特别的XML文本,即 UpdateGram, 利用它直接把更新发送到数据库。 在 VFP7 中,可以用 XMLUpdateGram 函数建立这个文本。 用VFP 8 的 CursorAdapter , UpdateGram 属性会自动建立。
第一个步骤是设置 updatable 属性并且建立一个更新指令。 在类定义的开始设置这些属性,并在CursorAdapter的init事件中增加一行代码,提供调用更新命令的方法。
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...
注意,我们已经在init方法中替换了属性表中的 UpdateCmd 和 SelectCmd 设置,实际上最终结果是一样的。 不管怎样, 这一段代码的第一部份现在看起来很熟悉,在这里我们设置了 KeyFieldList ,table, UpdatableFieldList 和 UpdateNameList 属性。 没有这些属性设置,就不可能创建 UpdateGram 。
然后,我们建立 UpdateXML 方法作为 CursorAdapter 的 UpdateCmd 。 没有参数传递给 UpdateXML 方法,所以,所有决定更新的工作必须这个方法里处理。还有,因为XML 类型的 CursorAdapter 没有默认的更新机制, 你必须写代码把更新传递给XML数据源。
在这一段代码中,使用 XMLHTTP 对象传递更新到服务器。 通过LoadXML方法加载 UpdateGram 属性的内容到 XMLDOM( 在ACCESS方法中) 之内,打开到服务器的连接,设定请求的内容为XML, 然后发送 XMLDOM。所有的结果通过 XMLHTTP 对象的 ResponseText 属性返回,接着装载到 XMLDOM 中,并且分析错误信息。
如果没有提示错误,更新已经成功,而且过程结束。然而,如果有错误, 就会产生符合语法的错误信息文本,并且包含在一个自定义的ERROR函数中,这样 TableUpdate 函数就能找到更新失败的原因。 如果没有这些代码, 即使有问题的时候,TableUpdate 总是返回成功信息。
测试一下这段代码,运行 caXML 程序,在游标中更改一个字段, 然后在命令窗口中发出 TableUpdate。 如果 TableUpdate 成功, 你应该能在服务器上看到更新结果。如果 TableUpdate 失败,你需要用 AError 函数接收 SQL server产生的错误信息。
如果你对 UpdateGram 的内容感到好奇, 你可以一步步查看类的 UpdateXML 方法,并且查看 UpdateGram 属性的内容。 然而,如果你不到有关数据变化的方法中 (如 UpdateCmd , InsertCmd 或 DeleteCmd 属性内容) ,你也不能完全弄明白 UpdateGram 属性的内容。
list6显示当ContactName字段的客户ID被更改为‘CACTU' 后UpdateGram的内容。
正如你看到的,SQLXML 能够读取这个文本并且能够很容易地建立一个Update-SQL 语句, 然后发送到 SQL server。 updg:sync(同步更新)元素使更新立即发生;因此,如果你有多个表需要更新,你可以把它们关联起来放到一个 UpdateGram中, 确定把他们封装进这个元素,执行一次就可以全部更新。
结束语
在这一篇文章中,我们涉及了许多方面,讲了新的 CursorAdapter 类的四种表现形式。 你已经学到了如何通过 DataEnvironment 、 CursorAdapter buileder、类设计器、和PRG建立 CursorAdapter 。 你也已经了解了建立本地、ODBC、OLE DB或XML型 CursorAdapter 类的基本方法, 并且怎样使这些类可更新。
下一个步骤是考虑如何把这些类应用到你的程序中。 对我来说,觉得 CursorAdapter 应用到任何程序的 UI 层中效果很好,同时应用于需要大量过程代码的许多商业开发。CursorAdapter并不是一个最好的选择对象用于层来传递数据。因为它把所有的来自数据源的数据转化成一个不便于携带的游标。
然而,在一个使用 CursorAdapter 类进行商业开发的方案中,它能接收来自数据源的数据, 然后用标准的 VFP 命令和函数处理数据,这是因为它产生的是一个 VFP 游标。 数据可以被转换为更一个较适当的类型,例如XML。
CursorAdapter 的另一个有利条件是通用的 OOP 接口,不管需要存取的数据是什么类型。 甚至用XML,需要大量代码来使类可更新,我们仍然可以用使用 CursorFill取回数据,用 TableUpdate 更新数据, 而且用 AError 返回错误, 像使用其他类型的 CursorAdapter一样。
基于一个事先的考虑或者计划,你想建立一个可以重复使用的CursorAdapter类。 这些类可以在程序之间重复使用或者在同一程序内部混合使用,来形成一种你的程序处理数据的标准方法。
有网友问使用cursoradapter是否还需要tableupdate(),下面索性再发一篇文章:
如何通过 CursorAdapter 对象使用 TableUpdate 函数更新数据
这一篇文章适用于:vfp 8.0
摘要:
这一篇文章讨论下列各项主题:
  1、TableUpdate 交互地使用 CursorAdapter 类更新后端数据。
  2、使用vfp 8.0 的新 CursorAdapter 类通过TableUpdate 函数更新数据的时候如何处理更新冲突。
较多的信息:
可以通过vfp 8.0 的 CursorAdapter 类取回来自本地的或远程的数据源的数据。 默认情况下,CursorAdapter 产生的游标不更新后端数据。 为了要更新后端数据,必须设定 CursorAdapter 的下列各项属性值:
  InsertCmd
  UpdateCmd
  DeleteCmd
如果你不设定这些属性值,你能通过设定下列各项 CursorAdapter 属性值自动地产生后端 SQL 更新指令:
  tables
  KeyFieldList
  UpdatableFieldList
  UpdateNameList
  SendUpdates
处理更新冲突
当你使用 CursorAdapter更新后端数据的时候, TableUpdate 函数返回 CursorAdapter 游标的更新结果。 如果一个更新冲突发生,后端数据的更新不可能成功。 然而, TableUpdate 可能仍然返回更新成功,因为在 CursorAdapter 游标中的数据被更新。  
更新冲突是指,使用者试图编辑一条取回后已经发生改变的记录。 下列各项是更新冲突能发生的情况:
1. User1 打开由客户表建立的一个游标(cursor)。
2. User2 更新第一条记录而且提交更新。
3. User1 更新第一条记录(使用 TableUpdate() 函数).
此时, User1 有一个更新冲突:  User1 正在尝试更新的后端记录在被取回之后已经发生改变。
DataSourceType 代码示例:
下列示例代码使用vfp 8.0 的native DataSourceType 更新 SQL server Northwind 数据库的一笔记录。 查证是否记录被更新,在更新命令中附加下列代码:
CRLF+[EXECSCRIPT+(" if _tally=0"+CHR(10)"+error ('update conflit')"+CHR(10)+"ENDIF")]
在这种情况下,Tableupdate() 返回 (.F。 ) 而且允许你处理更新失败。
#DEFINE CRLF CHR(13)+CHR(10)
Local loCursor,ovfp
CLEAR

ON ERROR

Set Exclusive Off
Close Databases All
Set Multilocks On
loCursor = Createobject('CA')
* Load loCursor with the data specified in SelectCmd and display error message if error occurs.
loCursor.CursorFill()
GO top
* Display the value of the company name before update.
? "Before:",companyname
?
ovfp=Createobject("visualfoxpro.application.8")
ovfp.DoCmd("set exclusive off")
ovfp.DoCmd("update (_samples+'\northwind\customers') set companyname='Alfreds Futterkisted' where customerid='ALFKI'")
GO top
* Update the data in the cursor.
replace companyname WITH 'Alfreds Futterkiste'
* Update the back end.
retval=TABLEUPDATE(0,.F.,locursor.alias)
Messagebox("Tableupdate="+Transform(retval))

* If update conflict occurs, display the error.
if(retval=.F.)
LOCAL ARRAY errors(1)
AERROR(errors)
* Displays the errors.
IF "Update conflict"$ errors[2]
  MESSAGEBOX("Update Conflict-reverting changes")
  =TABLEREVERT(.T.,locursor.alias)
ENDIF
endif
* Refresh the Cursor to get the updated data.
loCursor.CursorRefresh()  && Get the data again to be sure
GO top
* Display the value of the company name after update.
?
? "After:",companyname

Define Class CA As CursorAdapter
Alias = 'test1'
DataSourceType = 'NATIVE'
SelectCmd = 'select * from (_samples+"\northwind\customers")'
Tables = 'Customers'
KeyFieldList = "customerid"
UpdatableFieldList = "companyname"
UpdateNameList = "customerid customers.customerid,companyname customers.companyname"
WhereType= 3
* This is a custom property, that is added to handle update conflicts. It does not do
* anything by itself. It is added below to the automatically-generated UpdateInsertCmd to
* test whether anything was actually updated.
ConflictCheckCmd =CRLF+[EXECSCRIPT("IF _tally=0" + CHR(10) + "ERROR('Update conflict')" + CHR(10) + "ENDIF")]

Procedure AfterUpdate
  Lparameters cFldState, lForce, nUpdateType, UpdateInsertCmd, DeleteCmd, lResult
  * To see why it will fail on the back end, look at the UpdateInsertCmd that is used
  ? "Update Command sent="+UpdateInsertCmd
* Swap the actual values in the command to see what occurred.
  UpdateInsertCmd=Strtran(UpdateInsertCmd,[OLDVAL('customerid','test1')],Oldval('customerid','test1'))
  UpdateInsertCmd=Strtran(UpdateInsertCmd,[OLDVAL('companyname','test1')],Oldval('companyname','test1'))
  UpdateInsertCmd=Strtran(UpdateInsertCmd,[test1.companyname],test1.companyname)
  ? "With the OLDVAL() and test1.companyname evaluated the update statement is :"+UpdateInsertCmd
  * Check tally.
  ? "Tally="+Transform(_Tally)

Procedure BeforeUpdate
  Lparameters cFldState, lForce, nUpdateType, cUpdateInsertCmd, cDeleteCmd
  cUpdateInsertCmd=cUpdateInsertCmd+this.ConflictCheckCmd

ENDDEFINE
使用SQL server DataSourceType的示例代码:
下列代码使用 SQL server DataSourceType 更新 SQL server Northwind 示例数据库的一笔记录。 查证记录是否被更新,把下列代码加入更新指令:
IF @@ROWCOUNT=0 RAISERROR (' Update
    conflict.', 16, 1)
在这种情况下,Tableupdate() 返回 (.F。 ) 而且允许你处理更新失败。
LOCAL loCursor,ovfp,nhnd,lsuccess
CLEAR
SET EXCLUSIVE OFF
CLOSE DATABASES ALL
SET MULTILOCKS ON
loCursor = CREATEOBJECT('CA')
* Load loCursor with the data specified in SelectCmd and display error message if error occurs.
IF !loCursor.CursorFill()
=AERROR(lar)
MESSAGEBOX(lar[2])
ENDIF

* Display the value of the company name before update.
? "Company Name Before Update:",companyname
?
* Create a connection handle for SQL Server so you can set up the update conflict.
nhnd=SQLSTRINGCONNECT([Driver=SQL Server; SERVER=<SQL SERVER NAME>; DATABASE=NORTHWIND])
=SQLEXEC(nhnd,[update customers set companyname='Alfreds Futterkiste' where customerid='ALFKI'])
=SQLDISCONNECT(nhnd)
* Now make a change to the local data, and then try to update it.
GO TOP
REPLACE companyname WITH 'Alfreds Futterkisted'
lsuccess=TABLEUPDATE(0,.F.,locursor.alias)
Messagebox("Tableupdate="+Transform(lsuccess))
* Error handling function. Displaying the error message if update conflict occurs.
IF !lsuccess
=AERROR(lar)
IF "Update conflict"$ lar[2]
  MESSAGEBOX("Update conflict!-Reverting changes")
  =TABLEREVERT(.f.,locursor.alias)
ENDIF
ENDIF  
* Get the current data from the CursorAdapter.
loCursor.CursorRefresh()  
GO TOP
* Displaying the value of the company name after update.
?
?"Company Name After Update:", companyname
DEFINE CLASS CA AS CursorAdapter
Alias = 'test1'
SelectCmd = 'select * from customers'
Tables = 'Customers'
KeyFieldList = "Customerid"
UpdatableFieldList = "companyname"
UpdateNameList = "customerid customers.customerid,companyname customers.companyname"
WhereType= 3 && Key and modified

* This is a custom property that is added to handle update conflicts. It does not do
    * anything by itself. It is added below to the automatically-generated UpdateInsertCmd to
    * test whether anything was actually updated.
ConflictCheckCmd =";IF @@ROWCOUNT=0 RAISERROR (' Update conflict.', 16, 1)"


* Initializing the connectivity to Data source (SQL Server) by using ODBC driver.
PROCEDURE init
  WITH THIS
   .DataSourceType = 'ODBC'
   .DataSource=SQLSTRINGCONNECT([Driver=SQL Server; SERVER=<SQL SERVER NAME>; DATABASE=NORTHWIND])
  ENDWITH
ENDPROC

PROCEDURE BeforeUpdate
  LPARAMETERS cFldState, lForce, nUpdateType, cUpdateInsertCmd, cDeleteCmd
  
  ? "Entering BeforeUpdate()"
  
  cUpdateInsertCmd=cUpdateInsertCmd + THIS.ConflictCheckCmd
ENDPROC


PROCEDURE AfterUpdate
  LPARAMETERS cFldState, lForce, nUpdateType, UpdateInsertCmd, DeleteCmd, lResult
  * To see why it will fail on the back-end, look at the UpdateInsertCmd that is used.
  ? "Update Command sent="+UpdateInsertCmd
  * Swap the actual values in the command to see what occurred.
  lcActualCmd =Strtran(UpdateInsertCmd,[OLDVAL('customerid','test1')],Oldval('customerid','test1'))
  lcActualCmd =Strtran(UpdateInsertCmd,[OLDVAL('companyname','test1')],Oldval('companyname','test1'))
  lcActualCmd =Strtran(UpdateInsertCmd,[test1.companyname],test1.companyname)
  ? "With the OLDVAL() and test1.companyname evaluated the update statement is :"+UpdateInsertCmd
  ?
  ? "Leaving AfterUpdate()"
ENDPROC

* Destroying the connection.
PROCEDURE destroy
  =SQLDISCONNECT(THIS.DataSource)
ENDDEFINE
注意:这种更新方式(如这一段代码所示的)不支持批量更新。 (例如,cursoradapter.batchupdatcount>1) 。使用批量更新的时候,下列各项事件不动作:
  BeforeInsert
  AfterInsert
  BeforeUpdate
  AfterUpdate
  BeforeDelete
  AfterDelete
叁考
更多的信息,vfp8帮助中的下列各项主题。
“CursorAdapter Object Properties, Methods, and Events"
"Data Access Management Using CursorAdapters"
"Detecting and Resolving Conflicts"
"Locking Data"
"Management of Updates with Views"
"Programming for Shared Access"
vfp8自带的cursoradapter帮助:
用 Visual FoxPro,可以用cursor adapters接收支持以下数据源类型的本地或远程数据:
&#8226; 本地的
&#8226; 开放式数据源连接(ODBC)
&#8226; ActiveX 数据对象 (ADO)
&#8226; 扩展标记语言 (XML)
Cursoradapter 类为不同的数据源类型作为本地临时表使用提供支持. Cursoradapter 对象具有以下功能:
&#8226; 灵活地使用不同的数据源.
&#8226; 使用 Cursoradapter对象数据源或者数据环境 .
&#8226; 在数据源限制范围内分享数据.
&#8226; 随意定义Cursoradapter 对象生成的临时表的表结构.
&#8226; 控制 Cursoradapter 对象生成的临时表的数据装载.
&#8226; 从不同数据源生成基于数据源类型的Visual FoxPro 临时表.
&#8226; 用 Cursoradapter 属性和方法控制怎样添加、更新、删除数据.
&#8226; 除数据环境以外,添加 Cursoradapter 对象到表单、表单集和其他容器.
&#8226; 用 Cursoradapter 类作为单独的、无数据环境的类.
对 Cursoradapter 对象来说,数据源只是一个翻译层,它从数据源获取数据生成 Visual FoxPro 临时表.
注意:   Visual FoxPro 不支持Cursoradapter 对象建立连接关系. 但是, 可以在生成的临时表之间建立连接关系.
想了解更多 Cursoradapter, 数据环境, 和游标类的信息,请看: Cursoradapter Class, DataEnvironment Object, 和 Cursor Object.
用TABLEUPDATE()和TABLEREVERT()函数实现数据互动
TABLEUPDATE( ) 函数能够识别并且用 Cursoradapter 对象工作. TABLEUPDATE( ) delegates its operations to the cursor adapter associated with the cursor. TABLEREVERT( ) operates on Cursoradapter objects in the same way as other buffered cursors.
想了解更多的 Cursoradapter 对象怎样影响TABLEUPDATE( ) 和 TABLEREVERT( ) 函数的行为, 请查看 TABLEUPDATE( ) Function 和
TABLEREVERT( ) Function.
Automatic Updating and Cursoradapters
Visual FoxPro在本地和远程视图中自动产生 SQL INSERT, UPDATE, 和 DELETE 命令. 用 Cursoradapter 对象,可以指定、控制Visual FoxPro 怎样操作SQL INSERT, UPDATE, 和 DELETE 命令.
当 Cursoradapter InsertCmd, UpdateCmd, 和 DeleteCmd 属性为空的时候, Visual FoxPro 自动产生标准的SQL命令. 你必须确定那些自动产生的命令是否适合使用的数据源.要自动产生SQL INSERT, UPDATE, 和 DELETE 命令,你必须设定下面的 Cursoradapter特定属性:
&#8226; Tables
&#8226; KeyFieldList
&#8226; UpdatableFieldList
&#8226; UpdateNameList
Tables 和 UpdateNameList 属性要符合以下规则:
&#8226; Tables
如果允许自动更新,必须按顺序列出希望在 SQL INSERT, UPDATE, 和 DELETE 命令中显示的表的名称.
&#8226; UpdateNameList
&#8226; 必须指定一个包含成对出现的、本地和完整的远程数据字段的列表,用分号分隔。 没一对字段名中,本地的在前,远程的在后。完整的远程字段名应该这样写:<远程表名>, <远程字段名>, <远程表名> 必须和Tables 属性中的名字一致.
如果你设置以下Cursoradapter 的属性为 True (.T.),必须指定关键字段:
&#8226; AllowInsert
&#8226; AllowUpdate
&#8226; AllowDelete
你想了解更多关于SQL 的自动更新命令,请看 INSERT - SQL Command, UPDATE - SQL Command, 和 DELETE - SQL Command.
批量更新(Batch Updates)
如果 Cursoradapter BatchUpdateCount 属性的值大于1, Cursoradapter 对象使用批量更新 。 同时必须具备以下条件:
&#8226; Cursoradapter 对象设置为所有允许的操作使用相同的ODBC连接,包括AllowInsert, AllowUpdate, 和 AllowDelete属性中设置的INSERT,UPDATE,和DELETE操作。.
&#8226; Cursoradapter 对象设置为所有允许的操作使用相同的ADODB 命令对象。
&#8226; Cursoradapter 对象所有被允许的操作使用 "XML" 作为数据源。
如果使用批量更新, 下列 Cursoradapter 事件不发生:
&#8226; BeforeInsert
&#8226; AfterInsert
&#8226; BeforeUpdate
&#8226; AfterUpdate
&#8226; BeforeDelete
&#8226; AfterDelete
如果批量更新失败, Visual FoxPro 尝试在该批数据中的每一行发送一个分散的更新,然而,列举的事件不会发生。
更多的相关信息, 参见 BatchUpdateCount Property.
自动更新(Automatic Updating)和 ActiveX Data Objects (ADO)
用 ADO工作的时候, 可以用两种办法发送更新:
&#8226; 使用 Cursoradapter 对象,通过在Cursoradapter CursorFill 方法中使用 ADO 记录集对象发送更新
&#8226; 当使用 ADO记录集执行自动更新的时候, 设置ursorAdapter InsertCmdDataSource, UpdateCmdDataSource, 和 DeleteCmdDataSource 属性的时候,必须遵循下列规则:
&#8226; ADO记录集必须是可读写的并且标记为可用。
&#8226; 当使用客户端记录集对象的时候,书签必须总是可用。
&#8226; Bookmarks might be available when using Keyset or Static server-side cursors when supported by the OLE DB Provider.
&#8226; 按照一个字段一个字段的规则进行更新.更新完一条记录以后, Cursoradapter 对象在ADO记录集中寻找原始记录t, 用来改变可更新字段的值, 并且调用ADO记录集的update方法。 Cursoradapter 对象不调用ado记录集的updatebatch方法, 因此, 你的程序需要在恰当的时候明确地调用 UpdateBatch方法。
当使用这个方法程序的时候, 需要按以下方法设置 Cursoradapter 对象:
THIS.DataSourceType="ADO"
THIS.UpdateCmdDataSourceType=""
可以用数据环境(DataEnvironment)和 Cursoradapter builders 来建立一个可更新的表单,或者使用数据环境 DataEnvironment 和 Cursoradapter 类库,或者在Cursoradapter Builder中使用 AutoUpdate tab 制定一个主关键字段,更新字段,或者其他要求的信息。
&#8226; 通过一个自定义的或者自动产生的命令,通过 Cursoradapter 对象向数据库直接发送一个更新。
&#8226; 当执行自动直接更新, Cursoradapter 对象需要一个 ADO对象,其 ActiveConnection 属性设置成开放的ADO连接模型。
&#8226; 用这种方法的时候, 需要用以下方法设置Cursoradapter对象 :
THIS.UpdateCmdDataSourceType="ADO"
THIS.UpdateCmdDataSource=NewADODBCommandObject
在这个方法里不推荐使用 THIS.DataSourceType="ADO" 。
自动更新和 XML
Cursoradapter 对象使用XML数据源时,不自动产生SQL INSERT, UPDATE, 和 DELETE命令,因为已经存在好多产生XML的手段。 然而,可以使用Cursoradapter 对象产生一个 XML UpdateGram ,放在 Cursoradapter UpdateGram 属性.
尽管Cursoradapter 对象能产生XML UpdateGram, 你必须使用恰当的协议来实现最终更新。例如,SQL XML(HTTP), SQL XML OLE DB, 或者 XML Web service to .NET.
使用 XML 数据源和产生 XML UpdateGrams的时候必须遵循以下规则:
&#8226; 必须在 Cursoradapter InsertCmd, UpdateCmd, and DeleteCmd 属性中指定具体的命令,Visual FoxPro能够据以进行正确的插入、更新、和删除操作。 如果使用批量更新, Cursoradapter BatchUpdateCount 属性值必须大于1, UpdateCmd 在批量中只执行一次。
&#8226; 下列 Cursoradapter 属性必须设置为 "XML":
&#8226; InsertCmdDataSourceType
&#8226; UpdateCmdDataSourceType
&#8226; DeleteCmdDataSourceType
如果这些属性不设置为"XML", Visual FoxPro 执行 UpdateCmd, InsertCmd, DeleteCmd 属性中的命令而且不产生XML UpdateGram.
&#8226; 要想正确的建立XML 更新(UpdateGram), Cursoradapter 需要 Tables, UpdatableFieldList, UpdateNameList 属性中包含确定的值。 Cursoradapter 可以通过在Tables, UpdatableFieldList,和 UpdateNameList 属性中指定恰当的名称来产生一个多表更新。因此, 可以更新用XMLUPDATEGRAM( )函数产生的连接多个表的游标。更多的信息,请看 XMLUPDATEGRAM( ) Function.
&#8226; Cursoradapter 使用 WhereType 属性产生XML UpdateGram 的 before 事件。因此, 当你执行一个更新或删除的操作时,   KeyFieldList 和 UpdatableFieldList 属性必须包含一个主关键字段.更多的信息,参见 WhereType Property.
&#8226; 如果设置了行缓冲, 或者 BatchUpdateCount 的值是1, Visual FoxPro 为每个update, insert, delete 操作建立一个XML UpdateGram 更新。
如果设置为表缓冲, 并且使用批量更新, 即atchUpdateCount 的值大于1, Visual FoxPro 为全部的批量创建一个 XML更新。 此种方式下, 必须使用TABLEUPDATE( ) 函数执行XML更新。
See Also
Working with Tables | Cursoradapter Class | Cursoradapter Object Properties, Methods, and Events

 

作者:pony
专家分:9640
发表时间:2004-6-16 21:47:00    [回复]  [引用]
4 楼  
谢谢,我一定好好的学习你发表的文章!!

 

签名档
学编程的过程好苦!

现在学的软件有VB,SQLSERVER 了!希望能和各位多交流!
  此帖尚未评分
作者:zszhm
专家分:50
发表时间:2004-6-21 14:41:00    [回复]  [引用]
5 楼  
关于使用后台数据的问题,俺是这样想的,VFP最大的优点就是自带一个数据解决方案以及其语句的简洁、易懂。如果向后台发展,其效力不及其他的工具如DELPH、VB等。

    所以,我的观点是不提倡用VFP时过多使用后台(也不是不用),这不是说不让人进步,真的想步入C/S或B/S时代,用VFP有点误人子弟,想想看,VFP发展到现在,才开始在数据库开发上有所成长,而其他的工具则己轻车熟路的向更多的数据库及网络技术发展了。
可能很多用VFP的人有这样的感觉,用VFP除了做本地数据操作不比其他工具差外,做其他的工作包括网络、数据库、网站等技术,有一种使不上力的感觉(因为也不是不行)。这取决于VFP是一个纯数据表工具。
欢迎批评!

 

签名档
心如白云常自在,意如流水任东西。
  此帖尚未评分
作者:huangdehua
专家分:5680
发表时间:2004-6-21 17:18:00    [回复]  [引用]
6 楼  
楼上的朋友可能有点误会了,我想你肯定没用过VFP作前台,其它大型数据库作后台开发C/S系统.我感觉VFP+SQL SERVER配合性能不比DELPHI差,尽管我对DELPHI不算内行(后来想转为C#,所以没深入研究),但我用过,老王也发表过一些观点,认为VFP+SQL SERVER 比 DELPHI+SQL SERVER 差

 

作者:zszhm
专家分:50
发表时间:2004-6-22 13:22:00    [回复]  [引用]
7 楼  
是这样的,楼主,用VFP连接SQL的机制及存储过程的使用及后台的使用,并非不行,差的是VFP本身的开发环境先天优于本地表的使用,对于DELPHI及VB、PB来说,其差别没有一点的参数说明,无法精确的说出其他工具强到什么程度,只是,包括发展到现在的VFP9在内(我不太了解),在于后台数据库、网络技术(包括并发控制)、系统控制等等的开发环境上,都有些牵强,但其它的工具先天在这些方面就是强势,所以,感觉上来说,用VFP开发本地数据应用是比较理想的,我也用过用VFP做的商业MRP ,不知是技术还是成本问题,仍是用本地表做的,但其多用户机制控制的相当好,没有出现冲突这种只有VFP才有的要命的问题。
    其实,也不是讨论VFP好不好的问题,而是说VFP到底怎么用才会更有效,以及怎样避免其缺点。这样对于同行和初学者,是终生受益的。不知楼主意下如何。

 

签名档
心如白云常自在,意如流水任东西。
  此帖尚未评分
作者:huangdehua
专家分:5680
发表时间:2004-6-22 19:57:00    [回复]  [引用]
8 楼  
vfp 访问 SQL SERVER 速度似乎比DELPHI 更快些

 

作者:xiaoli0911
专家分:590
发表时间:2004-6-28 21:49:00    [回复]  [引用]
9 楼  
两位的观点各有其词。
楼主的这篇文章早已拜读,并仿此改写几个小东东,收益匪浅。
用VFP自身的数据库有整体化一的好处,不需要在另外做个什么数据仓库了。
但确有其败病,比如构造WEB。虽说目前对VFP+WEB的研究有了质的突破,但绝大多数还是会以SQL为后台的,那么我们如何把处理好了的数据提交到WEB就成了我们所研究的一个问题。基于此,用VFP+SQL有其长。
另一则说,VFP的C/S或B/S的构造由于DBF自身的保密性差好象总让人不放心似的。
加之VFP8中CA的出台,VFP+SQL或许是今后程序员的偏爱,让VFP这个后娘生的(对MS来说)在数据库领域重放异彩。

 

 此帖被评20分
作者:huangdehua
专家分:5680
发表时间:2004-6-29 6:58:00    [回复]  [引用]
10 楼  
用VFP自身的数据库有整体化一的好处,不需要在另外做个什么数据仓库了。
----数据仓库的概念好象不是如此--查查SQL SERVER的数据仓库看看

 

 
 
3

评论Comments