VFP的数据策略:高级篇html
做者:Doug Hennig 翻译:老瓷node
在“VFP中的数据策略:基础篇”一文中,咱们研究了VFP应用程序中访问非VFP数据(如SQL Server)的不一样机制:远程视图、SQL Passthrough、ADO、XML和VFP 8中添加的CursorAdapter类。在本文中,咱们将更详细地讨论CursorAdapter,并讨论可重用数据类的概念。此外,咱们将简要介绍新的XMLAdapter基类,并了解它如何帮助与其余源(如ADO.NET)交换数据。web
在我看来,CursorAdapter是VFP 8中最大的新特性之一。我以为他们这么酷的缘由是:sql
最后是一个例子。假设您有一个应用程序使用带有CursorAdapter的ODBC来访问SQL Server数据,出于某种缘由,您但愿更改成使用ADO相反。您只需更改CursorAdapters的DataSourceType并更改到后端数据库的链接,就完成了。应用程序中的其余组件既不知道也不关心这一点;它们仍然看到同一个游标,而无论用于访问数据的机制如何。数据库
让咱们开始经过查看CursorAdapter的属性、事件和方法(PEMs)来检查它们。编程
这里咱们不讨论CursorAdapter类的全部属性、事件和方法,只讨论更重要些的属性、事件和方法。有关完整列表,请参阅VFP文档。
(PEMS:属性、事件、方法统称的缩写——译者注)后端
这个属性很重要:它决定了类的行为,以及将什么类型的值放入其余一些属性中。有效的选项是“Native”,这表示您使用的是Native表,或者是选择“ODBC”、“ADO”或“XML”,这表示您使用了适当的机制来访问数据。您可能不会使用“Native”,由于您可能会使用Cursor对象而不是CursorAdapter,但此设置将使之后升迁应用程序更容易。数组
这是访问数据的方法。当DataSourceType设置为“Native”或“XML”时,VFP忽略此属性。对于ODBC,将DataSource设置为有效的ODBC链接句柄(这意味着您必须本身管理链接)。对于ADO,数据源必须是一个ADO记录集,该记录集的ActiveConnection对象设置为打开的ADO链接对象(一样,您必须本身管理)。服务器
若是此属性设置为.T(默认值为.F),则能够不使用DataSourceType和DataSource属性,由于CursorAdapter将使用数据环境(DataEnvironment)的属性(VFP 8也将DataSourceType和DataSource添加到DataEnvironment类)。将此设置为.T.的一个示例是,但愿数据环境中的全部CursorAdapter使用相同的ODBC链接。并发
对于除了XML之外的全部内容,这是用于检索数据的SQL SELECT命令。对于XML,这能够是能够转换为游标的有效XML字符串(使用内部XMLTOCURSOR()调用)或返回有效XML字符串的表达式(如UDF)。
此属性保存游标的结构,其格式与您在CREATE Cursor命令中使用的格式相同(此类命令中括号之间的全部内容)。这里有一个例子:CUST_ID C(6),COMPANY C(30),CONTACT C(30),CITY C(25)。尽管能够将此项留空,并告诉CursorAdapter在建立游标时肯定结构,但若是将CursorSchema填充进来,效果会更好。首先,若是CursorSchema为空或不正确,则在打开窗体的数据环境时可能会出错,或者没法将字段从CursorAdapter拖放到窗体以建立控件。幸运的是,VFP附带的CursorAdapter构建器能够自动为您填充这个内容。
这些属性(默认为.T)决定是否能够执行删除、插入和更新,以及是否将更改发送到数据源。
若是但愿VFP使用游标中所作的更改自动更新数据源,则须要这些属性,这些属性的用途与视图的同名CursorSetProp()属性相同。KeyFieldList是一个逗号分隔的字段列表(不带别名),这些字段构成游标的主键。表是一个逗号分隔的表列表。UpdateableFieldList是一个逗号分隔的字段列表(没有别名),能够更新。UpdateNameList是一个逗号分隔的列表,它将游标中的字段名与表中的字段名相匹配。UpdateNameList的格式以下:CursorFieldName1 Table.FieldName1,CursorFieldName2 Table.FieldName2……请注意,即便UpdateableFieldList不包含表的主键的名称(由于您不但愿更新该字段),它也必须仍然存在于UpdateNameList中,不然更新将不起做用。
若是要特别控制VFP如何删除、插入或更新数据源中的记录,能够为这些属性集指定适当的值(将上面的*替换为Delete、Insert和Update)。
此方法建立游标并用数据源中的数据填充它(尽管能够经过.T.使NoData参数建立空游标)。对于第一个使用CursorSchema或.F中定义的模式的参数,传递.T。以从数据源建立适当的结构(在我看来,这种行为是相反的)。必须设置多锁,不然此方法将失败。若是CursorFill因为任何缘由失败,它将返回.F,而不是引起错误;使用AERROR()来肯定出了什么问题(尽管准备好进行一些深挖,由于您常常收到的错误消息不够具体,没法确切地告诉您问题是什么)。
此方法相似于ReQuery()函数:它刷新游标的内容。
几乎每一个方法和事件都有先后“钩子”事件,容许您自定义CursorAdapter的行为。例如,在AfterCursorFill中,能够为游标建立索引,使其始终可用。对于Before事件,能够返回.F.以防止触发它的操做发生(这与数据库事件相似)。
下面是一个示例(CursorAdapterExample.prg),它从SQL Server附带的Northwind数据库的Customers表中获取巴西客户的某些字段。游标是可更新的,所以若是您在游标中进行了更改,请将其关闭,而后再次运行程序,您将看到您的更改已保存到后端。
local loCursor as CursorAdapter, ; laErrors[1] loCursor = createobject('CursorAdapter') with loCursor .Alias = 'Customers' .DataSourceType = 'ODBC' .DataSource = sqlstringconnect('driver=SQL Server;' + ; 'server=(local);database=Northwind;uid=sa;pwd=;trusted_connection=no') .SelectCmd = "select CUSTOMERID, COMPANYNAME, CONTACTNAME " + ; "from CUSTOMERS where COUNTRY = 'Brazil'" .KeyFieldList = 'CUSTOMERID' .Tables = 'CUSTOMERS' .UpdatableFieldList = 'CUSTOMERID, COMPANYNAME, CONTACTNAME' .UpdateNameList = 'CUSTOMERID CUSTOMERS.CUSTOMERID, ' + ; 'COMPANYNAME CUSTOMERS.COMPANYNAME, CONTACTNAME CUSTOMERS.CONTACTNAME' if .CursorFill() browse else aerror(laErrors) messagebox(laErrors[2]) endif .CursorFill() endwith
为了支持新的CursorAdapter类,对DataEnvironment、Form类及其设计器进行了一些更改。
首先,如前所述,DataEnvironment类如今有DataSource和DataSourceType属性。它自己不使用这些属性,但已将UseDataSource设置为.T.的任何CursorAdapter成员都使用这些属性。其次,如今可使用类设计器(woo-hoo!)可视化地建立DataEnvironment子类。
至于表单,如今能够经过设置新的DEClass和DEClassLibrary属性来指定要使用的DataEnvironment子类。若是您这样作,您对现有数据环境所作的任何事情(游标、代码等)都将丢失,但至少您会首先收到警告。表单的一个很酷的新特性是BindControls属性;在属性窗口中将其设置为.F. 意味着VFP不会在初始化时尝试对控件进行数据绑定,只有在将BindControls设置为.T.时才会这样作。这有什么好处?好吧,您诅咒参数传递给Init多少次了,Init在全部控件初始化并绑定到它们的ControlSource以后触发?若是要将参数传递给告诉它要打开哪一个表的窗体或其余影响ControlSources的内容,该怎么办?这个新属性使这个问题很快解决。
CursorGetProp('SourceType')返回一个新的值范围:若是游标是用CursorFill建立的,则该值为100加上旧值(例如,远程数据为102)。若是游标是用CursorAttach建立的(容许您将现有游标附加到CursorAdapter对象),则该值为200加上旧值。若是数据源是ADO记录集,则值为104(CursorFill)或204(CursorAttach)。
VFP包括DataEnvironment和CursorAdapter构造器(或称为生成器——译者注),使得使用这些类更加容易。
以正常方式启动DataEnvironment Builder:在类设计器中右键单击窗体的DataEnvironment或DataEnvironment子类,而后选择Builder。数据环境生成器的“数据源”页是设置数据源信息的位置。选择所需的数据源类型和数据源的来源。若是选择“使用现有链接句柄”(ODBC)或“使用现有ADO记录集”(ADO),请指定包含数据源的表达式(例如“goConnectionMgr.nHandle”)。您还能够选择使用系统上的任一个DSN或链接字符串。只有在为ADO选择“使用链接字符串”时才会启用“生成”按钮,该按钮将显示“数据连接属性”对话框,您可使用该对话框直观地生成链接字符串。若是选择“使用DSN”或“使用链接字符串”,生成器将在数据环境的BeforeOpenTables方法中生成代码以建立所需的链接。若是选择“Native”,则能够选择VFP数据库容器做为数据源;在这种状况下,生成的代码将确保数据库是打开的(也可使用自由表做为数据源)。
“Cursors”页面容许您维护DataEnvironment的CursorAdapter成员(游标对象不会在生成器中显示,也不能添加它们)。Add按钮容许您向DataEnvironment添加CursorAdapter子类,而New则建立一个新的基类CursorAdapter。Remove删除Select CursorAdapter,Builder为所选CursorAdapter调用CursorAdapter Builder。您能够更改CursorAdapter对象的名称,但对于任何其余属性,都须要CursorAdapter生成器。
从快捷菜单中选择Builder也能够调用CursorAdapter生成器。“Properties”页显示对象的类和名称(只有在从DataEnvironment中调出生成器时才能更更名称,由于它对CursorAdapter子类是只读的)、它将建立的游标的别名、是否应该使用DataEnvironment的数据源以及链接信息(若是没有)。与DataEnvironment生成器同样,若是选择“使用DSN”或“使用链接字符串”,CursorAdapter生成器将生成代码以建立所需的链接(在本例中是CursorFill方法)。
“数据访问”页容许您指定SelectCmd、CursorSchema和其余属性。若是您指定了链接信息,能够单击SelectCmd的Build按钮来显示Select Command Builder,这样就能够轻松地建立SelectCmd。
Select命令生成器简化了构建一个简单的Select语句的工做。从“表格”下拉列表中选择所需的表格,而后将相应的字段移到选定的一侧。对于本机数据源,能够向“表”组合框中添加表(例如,若是但愿使用空闲表)。选择OK时,SelectCmd将填充适当的SQL SELECT语句。
单击游标模式的“生成”按钮,自动为您填写此属性。为了使其工做,生成器实际上建立了一个新的CursorAdapter对象,适当地设置了属性,并调用CursorFill来建立游标。若是您没有到数据源的实时链接,或者CursorFill因为某种缘由(例如无效的SelectCmd)失败,那么这显然行不通。
使用“自动更新”页设置VFP自动为数据源生成更新语句所需的属性。Tables属性是从SelectCmd中指定的表自动填充的,fields网格是从CursorSchema中的字段填充的。与视图设计器同样,能够经过检查网格中的相应列来选择哪些是关键字段,哪些字段是可更新的。还能够设置其余属性,例如在将游标发送到数据源以前转换游标某些字段中的数据的函数。
更新、插入和删除页面的外观几乎相同。它们容许您为更新、删除和插入属性集指定值。对于VFP不能自动生成update语句的XML,这一点尤其重要。
尽管很明显CursorAdapter的目的是为了标准化和简化对非VFP数据的访问,可是您能够经过将DataSourceType设置为“Native”来使用它来替代Cursor。你为什么这样作?主要是倾向于未来应用程序升级;经过简单地将DataSourceType更改成其余选项之一(并可能更改其余一些属性,如设置链接信息),您能够轻松地切换到其余DBMS,如SQL Server。
当DataSourceType设置为“Native”时,VFP将忽略DataSource。SelectCmd必须是一个SQL SELECT语句,而不是USE命令或表达式,这意味着您老是使用至关于本地视图的语句,而不是直接使用表。您须确保VFP可找到SELECT语句中引用的任何表,所以若是这些表不在当前目录中,则须要设置路径或打开表所属的数据库。与往常同样,若是但愿游标可更新,请确保设置更新属性(KeyFieldList、Tables、UpdateableFieldList和UpdateNameList)。
如下示例(NativeExample.prg)从TestData VFP示例数据库中的Customer表建立一个可更新的游标:
local loCursor as CursorAdapter, ; laErrors[1] open database (_samples + 'data\testdata') loCursor = createobject('CursorAdapter') with loCursor .Alias = 'customercursor' .DataSourceType = 'Native' .SelectCmd = "select CUST_ID, COMPANY, CONTACT from CUSTOMER " + ; "where COUNTRY = 'Brazil'" .KeyFieldList = 'CUST_ID' .Tables = 'CUSTOMER' .UpdatableFieldList = 'CUST_ID, COMPANY, CONTACT' .UpdateNameList = 'CUST_ID CUSTOMER.CUST_ID, ' + ; 'COMPANY CUSTOMER.COMPANY, CONTACT CUSTOMER.CONTACT' if .CursorFill() browse tableupdate(1) else aerror(laErrors) messagebox(laErrors[2]) endif .CursorFill() endwith close databases all
ODBC其实是DataSourceType的四个设置中最直接的一个。将DataSource设置为打开的ODBC链接句柄,设置经常使用属性,而后调用CursorFill来检索数据。若是您填写KeyFieldList、Tables、UpdateableFieldList和UpdateNameList,VFP将自动生成适当的UPDATE、INSERT和DELETE语句,以便用任何更改更新后端。若是要改用存储过程,请适当设置*Cmd、*CmdDataSource和*CmdDataSourceType属性。
下面是一个示例,取自ODBCExample.prg,它调用Northwind数据库中的CustOrderHist存储过程,以获取特定客户按产品销售的总单位:
local loCursor as CursorAdapter, ; laErrors[1] loCursor = createobject('CursorAdapter') with loCursor .Alias = 'CustomerHistory' .DataSourceType = 'ODBC' .DataSource = sqlstringconnect('driver=SQL Server;server=(local);' + ; 'database=Northwind;uid=sa;pwd=;trusted_connection=no') .SelectCmd = "exec CustOrderHist 'ALFKI'" if .CursorFill() browse else aerror(laErrors) messagebox(laErrors[2]) endif .CursorFill() endwith
使用ADO做为CursorAdapter的数据访问机制比使用ODBC有更多的问题:
下面的示例代码取自ADOExample.prg,它展现了如何在ADO命令对象的帮助下使用参数化查询检索数据。这个例子还展现了VFP 8中新的结构化错误处理特性的使用;对ADO链接Open方法的调用封装在一个TRY…CATCH…ENDTRY语句捕获方法失败时将引起的COM错误。
local loConn as ADODB.Connection, ; loCommand as ADODB.Command, ; loException as Exception, ; loCursor as CursorAdapter, ; lcCountry, ; laErrors[1] loConn = createobject('ADODB.Connection') with loConn .ConnectionString = 'provider=SQLOLEDB.1;data source=(local);' + ; 'initial catalog=Northwind;uid=sa;pwd=;trusted_connection=no' try .Open() catch to loException messagebox(loException.Message) cancel endtry endwith loCommand = createobject('ADODB.Command') loCursor = createobject('CursorAdapter') with loCursor .Alias = 'Customers' .DataSourceType = 'ADO' .DataSource = createobject('ADODB.RecordSet') .SelectCmd = 'select * from customers where country=?lcCountry' lcCountry = 'Brazil' .DataSource.ActiveConnection = loConn loCommand.ActiveConnection = loConn if .CursorFill(.F., .F., 0, loCommand) browse else aerror(laErrors) messagebox(laErrors[2]) endif .CursorFill(.F., .F., 0, loCommand) endwith
将XML与CursorAdapter结合使用须要一些额外的东西。如下是问题:
游标的XML源能够来自不一样的地方。例如,能够调用一个UDF,该UDF使用CURSORTOXML()将VFP游标转换为XML,并返回结果:
use CUSTOMERS cursortoxml('customers', 'lcXML', 1, 8, 0, '1') return lcXML
UDF能够调用返回结果集为XML的Web服务。下面是一个从我在本身的系统上建立和注册的Web服务中为我生成的自动感应示例(细节并不重要;它只是显示了一个Web服务的示例)。
local loWS as dataserver web service loWS = NEWOBJECT("Wsclient",HOME()+"ffc\_webservices.vcx") loWS.cWSName = "dataserver web service" loWS = loWS.SetupClient("http://localhost/SQDataServer/dataserver.WSDL", ; "dataserver", "dataserverSoapPort") lcXML = loWS.GetCustomers() return lcXML
它可使用SQLXML 3.0执行存储在Web服务器模板文件中的SQL Server 2000查询(有关SQLXML的更多信息,请访问http://msdn.microsoft.com并搜索SQLXML)。下面的代码使用MSXML2.XMLHTTP对象经过HTTP从Northwind Customers表中获取全部记录;稍后将详细解释这一点。
local loXML as MSXML2.XMLHTTP loXML = createobject('MSXML2.XMLHTTP') loXML.open('POST', 'http://localhost/northwind/template/' + ; 'getallcustomers.xml, .F.) loXML.setRequestHeader('Content-type', 'text/xml') loXML.send() return loXML.responseText
处理更新更为复杂。数据源必须可以接受和使用diffgram(与SQL Server 2000同样),或者您必须本身找出更改并发出一系列SQL语句(UPDATE、INSERT和DELETE)来执行更新。
下面是一个示例(XMLExample.prg),它使用带有XML数据源的CursorAdapter。注意,SelectCmd和UpdateCmd都调用UDF。在SelectCmd的状况下,SQL Server 2000 XML模板的名称和要检索的客户ID被传递给一个名为GetNWXML的UDF,稍后咱们将讨论这个UDF。对于UpdateCmd,VFP将UpdateGram属性传递给SendNWXML,咱们稍后也将查看该属性。
local loCustomers as CursorAdapter, ; laErrors[1] loCustomers = createobject('CursorAdapter') with loCustomers .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)' .DataSourceType = 'XML' .KeyFieldList = 'CUSTOMERID' .SelectCmd = 'GetNWXML([customersbyid.xml?customerid=ALFKI])' .Tables = 'CUSTOMERS' .UpdatableFieldList = 'CUSTOMERID, COMPANYNAME, CONTACTNAME, ' + ; 'CONTACTTITLE, ADDRESS, CITY, REGION, POSTALCODE, COUNTRY, PHONE, FAX' .UpdateCmdDataSourceType = 'XML' .UpdateCmd = 'SendNWXML(This.UpdateGram)' .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' if .CursorFill(.T.) browse else aerror(laErrors) messagebox(laErrors[2]) endif .CursorFill(.T.) endwith
此代码引用的XML模板CustomersByID.XML以下所示:
<root xmlns:sql="urn:schemas-microsoft-com:xml-sql"> <sql:header> <sql:param name="customerid"> </sql:param> </sql:header> <sql:query client-side-xml="0"> SELECT * FROM Customers WHERE CustomerID = @customerid FOR XML AUTO </sql:query> </root>
将此文件放在Northwind数据库的虚拟目录中(有关配置IIS以使用SQL Server的详细信息,请参阅附录)。
这是GetNWXML的代码。它使用MSXML2.XMLHTTP对象访问Web服务器上的SQL Server 2000 XML模板并返回结果。模板的名称(以及可选的任何查询参数)做为参数传递给此代码。
lparameters tcURL local loXML as MSXML2.XMLHTTP loXML = createobject('MSXML2.XMLHTTP') loXML.open('POST', 'http://localhost/northwind/template/' + tcURL, .F.) loXML.setRequestHeader('Content-type', 'text/xml') loXML.send() return loXML.responseText
SendNWXML看起来很类似,只是它但愿传递一个diffgram,将diffgram加载到MSXML2.DOMDocument对象中,并将该对象传递给Web服务器,而后Web服务器将经过SQLXML将其传递给SQL Server 2000进行处理。
lparameters tcDiffGram local loDOM as MSXML2.DOMDocument, ; loXML as MSXML2.XMLHTTP loDOM = createobject('MSXML2.DOMDocument') loDOM.async = .F. loDOM.loadXML(tcDiffGram) loXML = createobject('MSXML2.XMLHTTP') loXML.open('POST', 'http://localhost/northwind/', .F.) loXML.setRequestHeader('Content-type', 'text/xml') loXML.send(loDOM)
要了解其工做原理,请运行XMLExample.prg。您应该在浏览窗口中看到一条记录(ALFKI客户)。更改某个字段中的值,而后关闭窗口并再次运行PRG。您应该看到您的更改已写入后端。
与VFP中一般的状况同样,我建立了CursorAdapter和DataEnvironment的子类,我将使用这些子类而不是基类。
SFCursorAdapter(在SFDataClasses.vcx中)是CursorAdapter的一个子类,它添加了一些附加功能:
让咱们来看看这个类。
Init方法建立两个集合(使用新的集合基类,它维护事物的集合),一个用于SelectCmd属性可能须要的参数,另外一个用于在游标打开后应自动建立的标记。它还设置了MULTILOCKS on,由于这是CursorAdapter游标所必需的。
with This * 建立参数和标记集合 .oParameters = createobject('Collection') .oTags = createobject('Collection') * 确保 MULTILOCKS 设置为 on. set multilocks on endwith
AddParameter方法向parameters集合添加一个参数。向此方法传递参数的名称(该名称应与SelectCmd属性中显示的名称匹配)和可选的参数值(若是如今不传递,能够稍后使用GetParameter方法进行设置)。这段代码展现了VFP 8中的两个新特性:新的Empty类(没有PEMs),使其成为轻量级对象的理想选择;ADDPROPERTY()函数(其做用相似于那些没有该方法的对象的ADDPROPERTY方法)。
lparameters tcName, ; tuValue local loParameter loParameter = createobject('Empty') addproperty(loParameter, 'Name', tcName) addproperty(loParameter, 'Value', tuValue) This.oParameters.Add(loParameter, tcName)
使用GetParameter方法返回一个特定的参数对象;当您想设置要用于参数的值时,一般会使用这个方法。
lparameters tcName local loParameter loParameter = This.oParameters.Item(tcName) return loParameter
SetConnection方法用于将DataSource属性设置为所需的链接。若是DataSourceType是“ODBC”,请传递链接句柄。若是是“ADO”,则数据源须要是一个ADO记录集,其ActiveConnection属性设置为打开的ADO链接对象,所以经过Connection对象,SetConnection将建立记录集并将其ActiveConnection设置为传递对象。
lparameters tuConnection with This do case case .DataSourceType = 'ODBC' .DataSource = tuConnection case .DataSourceType = 'ADO' .DataSource = createobject('ADODB.RecordSet') .DataSource.ActiveConnection = tuConnection endcase endwith
要建立游标,请调用GetData方法而不是CursorFill,由于它会自动处理参数和错误。若是要建立游标但不填充数据,请将.T.传递给GetData。此方法所作的第一件事是建立私有范围的变量,这些变量的名称和值与参数集合中定义的参数相同(从这里调用的GetParameterValue方法返回参数对象的值或以“=”开头的值的求值)。接下来,若是咱们使用ADO而且有任何参数,代码将建立一个ADO Command对象并将其ActiveConnection设置为Connection对象,而后将Command对象传递给CursorFill方法;CursorAdapter要求在参数化ADO查询中使用该方法。若是咱们没有使用ADO或者没有任何参数,代码只调用cursor fill来填充游标。注意.T.被传递给CursorFill,告诉它在CursorSchema被填充时使用CursorSchema(这是我但愿基类具备的行为)。若是建立了游标,则代码调用CreateTags方法为游标建立所需的索引;若是没有,则调用HandleError方法来处理发生的任何错误。
lparameters tlNoData local loParameter, ; lcName, ; luValue, ; llUseSchema, ; loCommand, ; llReturn with This *若是咱们要填充游标(而不是建立空游标),则建立变量来保存任何参数 *必须在这里而不是在方法中这样作,由于咱们但愿它们的做用域是私有的 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 *尝试填充游标 llReturn = .CursorFill(llUseSchema, tlNoData, .nOptions) endif '?' $ .SelectCmd ... *若是咱们建立了游标,请建立为其定义的任何标记。 *若是没有,请处理错误。 if llReturn .CreateTags() else .HandleError() endif llReturn endwith return llReturn
Update方法很简单:它只调用TABLEUPDATE()尝试更新原始数据源,若是失败则调用HandleError。
local llReturn llReturn = tableupdate(1, .F., This.Alias) if not llReturn This.HandleError() endif not llReturn return llReturn
有几种方法咱们在这里不看,你能够本身检查一下。AddTag将游标建立后要建立的索引的信息添加到tags集合,而CreateTags(从GetData调用)在INDEX ON语句中使用该集合中的信息。HandleError使用AERROR()来肯定出错的地方,并将错误数组的第二个元素放入cErrorMessage属性中。
让咱们看几个使用这个类的例子。第一个(取自TestCursorAdapter.prg)从Northwind数据库的Customers表中获取全部记录。这段代码与用于基类CursorAdapter的代码没有太大的不一样(因为没有填写CursorSchema,所以必须将.F.做为第一个参数传递给CursorFill)。
loCursor = newobject('SFCursorAdapter', 'SFDataClasses') with loCursor *链接到SQL Server Northwind数据库并获取客户记录 .DataSourceType = 'ODBC' .DataSource = sqlstringconnect('driver=SQL Server;server=(local);' + ; 'database=Northwind;uid=sa;pwd=;trusted_connection=no') .Alias = 'Customers' .SelectCmd = 'select * from customers' if .GetData() browse else messagebox('Could not get the data. The error message was:' + ; chr(13) + chr(13) + .cErrorMessage) endif .GetData() endwith
下一个示例(也取自TestCursorAdapter.prg)使用SFConnectionMgr的ODBC版原本管理链接,咱们在“VFP中的数据策略:基础篇”一文中查看了该版本。它还为SelectCmd使用参数化语句,显示AddParameter方法如何容许您处理参数,并演示如何使用AddTag方法自动为游标建立标记。
loConnMgr = newobject('SFConnectionMgrODBC', 'SFRemote') with loConnMgr .cDriver = 'SQL Server' .cServer = '(local)' .cDatabase = 'Northwind' .cUserName = 'sa' .cPassword = '' endwith if loConnMgr.Connect() loCursor = newobject('SFCursorAdapter', 'SFDataClasses') with loCursor .DataSourceType = 'ODBC' .SetConnection(loConnMgr.GetConnection()) .Alias = 'Customers' .SelectCmd = 'select * from customers where country = ?pcountry' .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() endwith else messagebox(loConnMgr.cErrorMessage) endif loConnMgr.Connect()
SFDataEnvironment(也在SFDataClasses.vcx中)比SFCursorAdapter简单得多,但添加了一些有用的功能:
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稍微复杂一点:它调用任何具备该方法且UseDEDataSource设置为.F.的成员对象的SetConnection方法,而后使用相似于SFCursorAdapter中的代码设置本身的数据源(若是任何CursorAdapter的UseDEDataSource设置为.T.)。
lparameters tuConnection local llSetOurs, ; loCursor, ; llReturn with This *调用任何不使用数据源的CursorAdapter的SetConnection方法 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 *若是发现使用数据源的CursorAdapter,须要设置数据源 if llSetOurs do case case .DataSourceType = 'ODBC' .DataSource = tuConnection case .DataSourceType = 'ADO' .DataSource = createobject('ADODB.RecordSet') .DataSource.ActiveConnection = tuConnection endcase endif llSetOurs endwith
Requery和Update几乎与GetData相同,因此咱们没必要费心去查看它们。
TestDE.prg显示了如何使用SFDataEnvironment做为两个SFCursorAdapter类的容器。因为此示例使用ADO,所以每一个SFCursorAdapter都须要本身的数据源,故UseDEDataSource设置为.F。请注意,对DataEnvironment SetConnection方法的单个调用负责为每一个CursorAdapter设置数据源属性。
loConnMgr = newobject('SFConnectionMgrADO', 'SFRemote') with loConnMgr .cDriver = 'SQLOLEDB.1' .cServer = '(local)' .cDatabase = 'Northwind' .cUserName = 'sa' .cPassword = '' endwith if loConnMgr.Connect() loDE = newobject('SFDataEnvironment', 'SFDataClasses') with loDE .NewObject('CustomersCursor', 'SFCursorAdapter', 'SFDataClasses') with .CustomersCursor .Alias = 'Customers' .SelectCmd = 'select * from customers' .DataSourceType = 'ADO' endwith .NewObject('OrdersCursor', 'SFCursorAdapter', 'SFDataClasses') with .OrdersCursor .Alias = 'Orders' .SelectCmd = 'select * from orders' .DataSourceType = 'ADO' endwith .SetConnection(loConnMgr.GetConnection()) 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 else messagebox(loConnMgr.cErrorMessage) endif loConnMgr.Connect()
如今咱们有了CursorAdapter和DataEnvironment子类,让咱们讨论一下可重用的数据类。
VFP开发人员要求微软在VFP中添加的一件事是可重用的数据环境。例如,您可能有一个表单和一个报表具备彻底相同的数据设置,可是您必须手动为每一个表单和报表填充数据环境,由于数据环境是不可重用的。一些开发人员(以及几乎全部的框架供应商)经过在代码中建立数据环境(它们不能可视化地被子类化)并在表单上使用“loader”对象来实例化数据环境子类,使得建立可重用的数据环境变得更加容易。然而,这是一种混乱,并无帮助报告。
如今,在VFP 8中,咱们可以建立两个可重用的数据类,它们能够提供从任何数据源到任何须要它们的数据源的游标,以及可重用的数据环境,后者能够托管数据类。在撰写本文时,您不能在报表中使用CursorAdapter或DataEnvironment子类,但能够经过编程添加CursorAdapter子类(例如在DataEnvironment的Init方法中)来利用那里的可重用性。
咱们来为Northwind客户和订单表建立数据类。首先,建立SFCursorAdapter的一个子类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生成器完成大部分工做,特别是设置CursorSchema和更新属性。诀窍是打开“use connection settings in builder only”(仅在生成器中使用链接设置)选项,填写链接信息以创建实时链接,而后填写SelectCmd并使用生成器为您构建其他属性。
如今,只要您须要Northwind Customers表中的记录,就只需使用CustomersCursor类。固然,咱们尚未定义任何链接信息,但这其实是件好事,由于这个类没必要担忧如何获取数据(ODBC、ADO或XML),甚至没必要担忧要使用什么数据库引擎(用于SQL Server、Access和新版VFP8的Northwind数据库)。
可是请注意,这个游标涉及Customers表中的全部记录。有时候,你只想要一个特定的客户。因此,让咱们建立一个CustomersCursor的子类CustomerByIDCursor。将SelectCmd更改成“select * from customers where customerid = ?pcustomerid”并将如下代码放入Init:
lparameters tcCustomerID dodefault() This.AddParameter('pCustomerID', tcCustomerID)
这将建立一个名为pCustomerID的参数(与SelectCmd中指定的名称相同),并将其设置为传递的任意值。若是未传递任意值,请使用GetParameter返回此参数的对象,并在调用GetData以前设置其Value属性。
建立一个相似于CustomersCursor的orderscorsor类,只是它从Orders表中检索全部记录。而后建立一个OrdersForCustomerCursor子类,该子类只检索特定客户的订单。将SelectCmd设置为“select * from orders where customerid = ?pcustomerid”,并将与CustomerByIDCursor相同的代码放入Init(由于它是相同的参数)。
要测试其效果,请运行TestCustomersCursor.prg。
如今咱们有了一些可重用的数据类,来用一下它们。首先,让咱们建立一个名为CustomersAndOrdersDataEnvironment的SFDataEnvironment子类,它包含CustomerByIDCursor和OrdersForCustomerCursor类。将AutoOpenTables设置为.F(由于咱们须要在打开表以前设置链接信息),并将CursorAdapter和UseDEDataSource设置为.T。如今能够以某种形式使用此数据环境来显示有关特定客户的信息,包括其订单。
让咱们建立这样一个表单。建立一个名为CustomerOrders.scx的表单(它包含在本文档附带的示例文件中),将DEClass和DEClassLibrary设置为CustomersAndOrdersDataEnvironment,以便咱们使用可重用的数据环境。将如下代码放入Load方法中:
#define ccDATASOURCETYPE 'ADO' with This.CustomersAndOrdersDataEnvironment *设置数据环境数据源 .DataSourceType = ccDATASOURCETYPE *若是咱们使用ODBC或ADO,请建立一个链接管理器 *并打开链接到Northwind数据库的链接 if .DataSourceType $ 'ADO,ODBC' This.AddProperty('oConnMgr') This.oConnMgr = newobject('SFConnectionMgr' + ccDATASOURCETYPE, ; 'SFRemote') with This.oConnMgr .cDriver = iif(ccDATASOURCETYPE = 'ADO', 'SQLOLEDB.1', ; 'SQL Server') .cServer = '(local)' .cDatabase = 'Northwind' .cUserName = 'sa' .cPassword = '' endwith if not This.oConnMgr.Connect() messagebox(oConnMgr.cErrorMessage) return .F. endif not This.oConnMgr.Connect() *若是咱们使用ADO,每一个游标都必须有本身的数据源 if .DataSourceType = 'ADO' .CustomerByIDCursor.UseDEDataSource = .F. .CustomerByIDCursor.DataSourceType = 'ADO' .OrdersForCustomerCursor.UseDEDataSource = .F. .OrdersForCustomerCursor.DataSourceType = 'ADO' endif .DataSourceType = 'ADO' *将数据源设置为链接 .SetConnection(This.oConnMgr.GetConnection()) *若是使用的是XML,请更改SelectCmd以调用GetNWXML函数 else .CustomerByIDCursor.SelectCmd = 'GetNWXML([customersbyid.xml?' + ; 'customerid=] + pCustomerID)' .CustomerByIDCursor.UpdateCmdDataSourceType = 'XML' .CustomerByIDCursor.UpdateCmd = 'SendNWXML(This.UpdateGram)' .OrdersForCustomerCursor.SelectCmd = 'GetNWXML([ordersforcustomer.' + ; 'xml?customerid=] + pCustomerID)' .OrdersForCustomerCursor.UpdateCmdDataSourceType = 'XML' .OrdersForCustomerCursor.UpdateCmd = 'SendNWXML(This.UpdateGram)' endif .DataSourceType $ 'ADO,ODBC' *指定将从CustomerID文本框中填充游标参数的值 loParameter = .CustomerByIDCursor.GetParameter('pCustomerID') loParameter.Value = '=Thisform.txtCustomerID.Value' loParameter = .OrdersForCustomerCursor.GetParameter('pCustomerID') loParameter.Value = '=Thisform.txtCustomerID.Value' *建立空游标并在失败时显示错误消息 if not .GetData(.T.) messagebox(.cErrorMessage) return .F. endif not .GetData(.T.) endwith
这看起来像不少代码,但其中大部分是为了演示目的,以容许切换到不一样的数据访问机制。
此代码建立一个链接管理器来处理链接(ADO、ODBC或XML),具体取决于ccDATASOURCETYPE常量,您能够更改该常量以尝试每一个机制。对于ADO,因为每一个CursorAdapter都必须有本身的数据源,所以为每一个CursorAdapter设置UseDEDataSource和DataSourceType属性。而后,代码调用SetConnection方法来设置链接信息。对于XML,SelectCmd、UpdateCmdDataSourceType和UpdateCmd属性必须如前所述进行更改。接下来,代码使用两个CursorAdapter对象的GetParameter方法将pCustomerID参数的值设置为表单中文本框的内容。注意在值中使用“=”;这意味着每次须要时都会对Value属性求值,所以咱们基本上有一个动态参数(当用户在文本框中键入时,保存将参数不断更改成当前值的须要)。最后,调用GetData方法来建立空游标,以便控件的数据绑定能够工做。
在表单上放置一个文本框并将其命名为txtCustomer,将如下代码放入其Valid方法中:
with Thisform .CustomersAndOrdersDataEnvironment.Requery() .Refresh() endwith
这将致使在输入客户ID时从新查询游标和刷新控件。
在表单上放置一个标签,放在文本框旁边,并将其标题设置为“客户ID”。
将CompanyName、ContactName、Address、City、Region、PostalCode和Country字段从DataEnvironment中的Customers游标拖动到表单中,以建立这些字段的控件。而后在Orders游标中选择OrderID、EmployeeID、OrderDate、RequiredDate、ShippedDate、ShipVia和Freight字段,并将它们拖到表单中以建立网格(Grid--译者注)。
就这样子。运行表单并输入“ALFKI”做为客户ID。当您在文本框中选择选项卡时,您应该会看到客户地址信息和订单。尝试更改有关客户或订单的内容,而后关闭表单,再次运行它,而后再次输入“ALFKI”。您应该看到,您所作的更改已写入后端数据库,而无需您付出任何努力。
很酷吧?这比基于本地表或视图建立表单要简单得多。更好的方法是,尝试将ccDATASOURCETYPE常量更改成“ADO”或“XML”,并注意表单的外观和工做方式彻底相同。这就是CursorAdapters的要点!
咱们试一个Report。此处讨论的示例取自此文档附带的CustomerOrders.frx。这里最大的问题是,与表单不一样,咱们不能告诉报表使用DataEnvironment子类,也不能在DataEnvironment中删除CursorAdapter子类。所以,咱们必须在报表中放入一些代码,以便将CursorAdapter子类添加到数据环境中。尽管将此代码放入报表数据环境的BeforeOpenTables事件中彷佛是合乎逻辑的,但实际上这不会起做用,由于我不明白为何,在预览报表时,BeforeOpenTables会在每一个页面上激发。因此,咱们将把代码放入Init方法中。
#define ccDATASOURCETYPE 'ODBC' with This set safety off *设置数据环境数据源 .DataSourceType = ccDATASOURCETYPE *为客户和订单建立CursorAdapter对象 .NewObject('CustomersCursor', 'CustomersCursor', 'NorthwindDataClasses') .CustomersCursor.AddTag('CustomerID', 'CustomerID') .NewObject('OrdersCursor', 'OrdersCursor', 'NorthwindDataClasses') .OrdersCursor.AddTag('CustomerID', 'CustomerID') *若使用ODBC或ADO,请建立一个链接管理器 *并打开链接到Northwind数据库的链接 if .DataSourceType $ 'ADO,ODBC' .AddProperty('oConnMgr') .oConnMgr = newobject('SFConnectionMgr' + ccDATASOURCETYPE, ; 'SFRemote') with .oConnMgr .cDriver = iif(ccDATASOURCETYPE = 'ADO', 'SQLOLEDB.1', ; 'SQL Server') .cServer = '(local)' .cDatabase = 'Northwind' .cUserName = 'sa' .cPassword = '' endwith if not .oConnMgr.Connect() messagebox(.oConnMgr.cErrorMessage) return .F. endif not .oConnMgr.Connect() *若是使用ADO,每一个游标都必须有本身的数据源 if .DataSourceType = 'ADO' .CustomersCursor.UseDEDataSource = .F. .CustomersCursor.DataSourceType = 'ADO' .CustomersCursor.SetConnection(.oConnMgr.GetConnection()) .OrdersCursor.UseDEDataSource = .F. .OrdersCursor.DataSourceType = 'ADO' .OrdersCursor.SetConnection(.oConnMgr.GetConnection()) else .CustomersCursor.UseDEDataSource = .T. .OrdersCursor.UseDEDataSource = .T. .DataSource = .oConnMgr.GetConnection() endif .DataSourceType = 'ADO' .CustomersCursor.SetConnection(.oConnMgr.GetConnection()) .OrdersCursor.SetConnection(.oConnMgr.GetConnection()) *若使用XML,请更改SelectCmd以调用GetNWXML函数 else .CustomersCursor.SelectCmd = 'GetNWXML([getallcustomers.xml])' .CustomersCursor.DataSourceType = 'XML' .OrdersCursor.SelectCmd = 'GetNWXML([getallorders.xml])' .OrdersCursor.DataSourceType = 'XML' endif .DataSourceType $ 'ADO,ODBC' *获取数据并在失败时显示错误消息 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() *设置从客户到订单的关系 set relation to CustomerID into Customers endwith
此代码看起来与窗体的代码相似。一样,大多数代码是处理不一样的数据访问机制。可是,还有一些额外的代码,由于咱们不能使用DataEnvironment子类,必须本身编写行为代码。
如今,咱们如何方便地把字段放在Report上?因为CursorAdapter在设计时不存在于数据环境中,所以咱们不能将字段从它们拖到Report中。这里有一个提示:建立一个PRG来建立游标并将其留在做用域中(经过挂起或使CursorAdapter对象公开),而后使用Quick Report函数将具备适当大小的字段放在Report上。
在CUSTOMERS.CUSTOMERID上建立一个组并选中“在新页面上启动每一个组”。而后将Report布局为相似于如下内容:
除了CursorAdapter以外,VFP 8还有三个新的基类来改进VFP对XML的支持:XMLAdapter、XMLTable和XMLField。XMLAdapter提供了一种在XML和VFP游标之间转换数据的方法。它的功能比CURSORTOXML()和XMLTOCURSOR()函数多得多,包括支持分层XML和使用那些函数不支持的XML类型(如ADO.NET数据集)的功能。XMLTable和XMLField是子对象,它们提供微调XML数据的模式的能力。此外,XMLTable还有一个ApplyDiffgram方法,它容许VFP使用updategrams和diffgrams,这是VFP 7中缺乏的。
为了让您了解它的功能,我建立了一个返回ADO.NET数据集的ASP.NET Web服务,而后使用VFP中的XMLAdapter对象来使用该数据集。如今我作到了。
首先,在Visual Studio.NET中,我将Northwind Customers表从服务器资源管理器拖到一个名为NWWebService的新ASP.NET Web服务项目中。这会自动建立两个对象,SQLConnection1和SQLDataAdapter1。而后,我将如下代码添加到现有生成的代码中:
<WebMethod()> Public Function GetAllCustomers() As DataSet Dim loDataSet As New DataSet() Me.SqlConnection1.Open() Me.SqlDataAdapter1.Fill(loDataSet) Return loDataSet End Function
我构建该项目是为了在NWWebService虚拟目录(VS.NET自动为我建立)中生成适当的Web服务文件。
为了在VFP中使用这个Web服务,我使用IntelliSense管理器注册了一个名为“Northwind.NET”的Web服务,指向“http://localhost/NWWebService/NWWebService.asmx?WSDL”做为WSDL文件的位置。而后我建立了如下代码(在XMLAdapterWebService.prg中)来调用Web服务并将ADO.NET数据集转换为VFP游标。
local loWS as Northwind.NET, ; loXMLAdapter as XMLAdapter, ; loTable as XMLTable *从.NET Web服务获取.NET数据集 loWS = NEWOBJECT("Wsclient",HOME()+"ffc\_webservices.vcx") loWS.cWSName = "Northwind.NET" loWS = loWS.SetupClient("http://localhost/NWWebService/NWWebService.asmx" + ; "?WSDL", "NWWebService", "NWWebServiceSoap") loXML = loWS.GetAllCustomers() *建立一个XMLAdapter并加载数据 loXMLAdapter = createobject('XMLAdapter') loXMLAdapter.XMLSchemaLocation = '1' loXMLAdapter.LoadXML(loXML.Item(0).parentnode.xml) *若是成功地加载了XML,那么从每一个表对象建立并浏览一个游标 if loXMLAdapter.IsLoaded for each loTable in loXMLAdapter.Tables loTable.ToCursor() browse use next loTable endif loXMLAdapter.IsLoaded
注意,为了使用XMLAdapter,您须要在系统上安装MSXML 4.0服务包1或更高版本。您能够从MSDN网站下载(http://MSDN.microsoft.com并搜索MSXML)。
我认为CursorAdapter是VFP 8中最大和最使人兴奋的加强之一,由于它提供了一个一致且易于使用的远程数据接口,并且它容许咱们建立可重用的数据类。我相信一旦你用它来工做,你会发现他们和我同样使人兴奋。
Doug Hennig是Stonefield Systems Group Inc.的合做伙伴。他是获奖的Stonefield数据库工具包(SDT)的做者和获奖的Stonefield查询的共同做者。他是《黑客视觉FoxPro 7.0指南》的合著者(与Tamar Granor、Ted Roche和Della Martin一块儿)和《视觉FoxPro 7.0的新特性》的合著者(与Tamar Granor和Kevin McNeish一块儿),均来自Hentzenwerke出版社,在Pinnacle Publishing的Pros Talk VisualFoxPro系列中,“VisualFoxPro数据字典”的做者。他在FoxTalk上写了每个月的“可重用工具”专栏。他是《黑客指南》和《基础知识》的技术编辑,这两本书都来自亨森沃克出版社。自1997年以来,道格在每次微软FoxPro开发者大会(DevCon)以及北美各地的用户团体和开发者大会上都发表过演讲。他是微软最有价值的专业人士(MVP)和认证专业人士(MCP)。
另文,本文略……