主绍
不少狐友(Foxers)都是从Dbase―FoxBase―Foxpro―VFP这样一条路走过来的,若是说从FoxBase 到Foxpro是一次飞跃,那么从Foxpro到VFP就是一次升华。漫漫编程路上的两次大变化都伴随着升级的兴奋与适应的痛苦,惯性思惟每每使咱们容易忽略新版本的新内容。咱们先来看看下面这个在表单中编辑记录的例子:
在Foxpro 2.X时代,我是这样设计的:
一、 在屏幕上放置与表中字段对应的文本框(TEXT)控件,用来存放相应的内存变量(如M.CUST_IDT和M.NAME等);
二、 当用户将表的指针定位到某一特定记录时(好比按了"下一个记录"按钮),就用scatter memvar 语句把该记录的全部字段传给相应的内存变量,再用show get来刷新屏幕上的文本框中显示的值。在这一时刻,用户是不能编辑这些变量的(此时文本框或者被设置为不可用(Disabled),或者其when子句返回的是.F.值),由于用户处在"浏览"状态。
三、 当用户选择了"编辑"按钮时,程序就锁定该记录(若是没法锁定,则显示提示信息),而后检测每一个字段值和相应的内存变量值(若是有不一样的,说明必定是有其它用户在咱们进入编辑状态后修改并保存了记录,在这种状况下,要显示提示信息),以后再用scatter memvar和show get语句刷新,这样用户就看到记录中的当前值了;
四、 若是全部字段和相应的内存变量彻底匹配,就把变量所在的文本框控件都设置为可用(Enabled)或都让when子句返回.T.,以便让用户能够来编辑这些变量;
五、 当用户选择了"保存"按钮时,根据必定的规则对输入的数据进行检验,而后用gatter memvar语句将内存变量写回到记录中,并对记录解锁(unlock),再把全部的内存变量所在文本框设置为不可用(Disabled),或者让其when子句返回.F.值,这时,用户又回到了"浏览"状态。
请注意,在这一过程当中咱们没有直接读写记录,而是先将各字段值赋给同名内存变量,再让用户编辑这些内存变量,若是一切正常再把这些内存变量写回到记录当中。之因此用这种方法主要是为了保护表,若是不符合验证规则,就不容许数据回存到表中。另外一点要注意的就是,当用户编辑记录时要对记录加锁(lock),这样就防止了其它用户在同一时间编辑同一条记录。可是,这种方法有一个很大的缺点:假设一个用户开始编辑某一记录,在他按下"保存"按钮前,记录一直处于锁定状态,若是此时该用户有事暂时外出,如午饭,那么其它用户就不能对该记录进行编辑了L。
固然,你能够不在进入"编辑"状态时加锁,而只在"保存"记录以前加锁,保存完后立刻解锁,这样可使记录被锁定时间最短,以便其它用户有充分的时间编辑该记录。可是这也有缺点,试想:若是用户编辑了内存变量,这后点击"保存",可若是其它用户在你点"保存"以前也编辑了这条记录,而且还没保存,这时会发生什么现象呢?你的保存要覆盖他人的修改吗?要放弃你的修改吗?这些都是咱们要在设计时认真考虑的问题。
咱们如此费尽心机地设计,其目的都是为了保护数据。若是你写的程序只是你一人使用,那设计起来可能会简单得多:你能够直接读取记录中的字段,好比你能够直接在屏幕中browse一个表,这样你输入的内容就直接写进记录了。可是,咱们不能担保那些最终用户也象您同样都清楚能输入什么不能输入什么,咱们不得不在用户与数据表之间创建一个"防火墙"来保护数据。在Foxpro 2.X中建立这样的"防火墙"要写一大堆的代码!
值得咱们高兴的是,VFP提供了内建的"防火墙"机制,它有两方面的做用:一是可直接读取记录,二是只容许经过全部检验规则的数据被写回。这一机制就是缓冲。
缓冲
在刚才咱们讲到的例子中,是经过内存变量存贮记录内容,这种方法能够被认为是手工创建了一个数据缓冲器,经过使用scatter memvar把数据从记录中传送到"缓冲"中,再用gather memvar从"缓冲"中传回到记录。
VFP不只能自动进行单条记录的缓冲(称为行缓冲或记录缓冲),并且还支持另外一种类型的缓冲,即表缓冲,表缓冲可经过缓冲器存取多条记录。行缓冲通常用于一次存取一个记录时,这种机制广泛应用于数据录入,就象前文提到的那样:用户能够在表单中显示或编辑单条记录。表缓冲则适用于一次更新多条记录,好比一张定货单的明细录入屏幕,经过对物品明细表使用表缓冲,能够容许用户编辑多条明细记录,最后一次性地将全部明细记录保存或放弃。
除了两种缓冲机制,VFP还有两种锁定机制。前文讲述的那种在Foxpro 2.X中的加锁方式被称为保守式锁定法(或悲观锁定法)――当用户选择"编辑"时加锁,直到用户选择了"保存"后再解锁。这种加锁机制确保了当本用户修改记录时其它用户都不能修改该记录,但这样作有利有弊,视具体状况而定。前文所讲的另外一种加锁方式叫作开放式加锁法(或乐观锁定法)――只有在写回记录时才锁定该记录,而后立刻解锁。这种加锁机制虽然使记录最大时间内可给别的用户使用,但咱们必须处理当两个用户同时编辑记录时所形成的冲突。正如咱们下面将要看到的,这对于VFP来讲真是太简单了,在VFP中大多数的状况下都选用开放式缓冲机制。
由于在VFP中记录能够被自动缓冲,因此就没必要再用"人工缓冲"机制了,换句话说,如今咱们能够直接读记录中的字段而没必要关心为每一个字段设内存变量。要保存修改,咱们只要简单地告诉VFP将缓冲器中的内容写到表中便可,如果取消修改,告诉VFP不写入便可。过会儿咱们将看到这是如何操做的。
当打开一个表时,VFP建立一个"临时表"(cursor)做为缓冲器,这个"临时表"用来定义表的属性。对于本地表来讲,"临时表"的惟一属性是用来定义缓冲方式的的,视图和远程表还有一些本文讨论范围以外的其它属性,这些属性的值用CursorSetProp()函数设置,用CursorGetProp()取得。咱们过会儿将看到如何使用这些函数。
当追加记录时,表缓冲有一个有趣的特性:随着记录添加到缓部器中,它们被赋予一个负记录号,第一个加入的记录,recno()返回值为-1,第二个返回值为-2,依此类推。你能够用一个带负数的go命令在缓冲中定位到追加的记录上。这在处理记录号时颇有用,好比为了确认变量InRecno是否为一个可用的记录号,在缓冲状态下,要检测between(InRecno,1,reccount()) OR InRecno<0 而不能只用between(InRecno,1,reccount())。
使用缓冲
缺省状况下缓冲器是关闭的,这种状况下在更新表时VFP和Foxpro 2.X是同样的,要使用缓冲你必需将它打开。缓冲适用于自由表及数据库中的表。要用缓冲还要set multilocks on,由于multilocks缺省值是OFF,若是你忘了设置其值为ON,会出现错误信息。你能够把multilocks=ON加入到CONFIG.FPW文件中,或是用"工具"栏下的"选项"功能保存其值为ON。
咱们经过cursorsetprop(‘Buffering’,<n>,<Alias>)来定义缓冲方式。若是是对当前表设置缓冲,没必要定义<Alias>,根据你想要的缓冲及锁定方式<n>为下列值:
缓冲及锁定法
<n>值
无缓冲
1
保守式行缓冲
2
开放式行缓冲
3
保守式表缓冲
4
开放式表缓冲
5 例如,要将当前表设置为开放式行缓冲,用cursorsetprop(‘Buffering’,3),要取得当前正打开的表的缓冲方式,用cursorgetprop(‘Buffering’)。
要设置一个表单的缓冲方式,你能够在表单的Load事件中用cursorsetprop()定义全部用到的表,但最好的方法是直接设置表单的BufferMode属性来决定是开放式仍是保守式(缺省值为"无"),这样设置后,该表单就会自动对绑定到网格(Grid)中的表使用表缓冲,对其它表则使用行缓冲。若是你的表单使用了数据环境,你能够对某一个表的BufferModeOverride属性按你的意图设置缓冲,以取表明单的BufferMode属性。
若是有用户正在修改缓冲记录中的数据(此时用户处在"编辑"状态),你不只能够取得他们输入到每一个字段中的值,还能取得每一个字段的初始值和当前值(此值为磁盘中的实际值),为此VFP提供了oldval()和curval()函数。
要获得:
使用:
用户输入值(缓冲的数据)
<fieldname> OR <alias.fieldname>
用户未作任何改动以前的值
Oldval(‘<fieldname>’)
当前记录中的值
Curval(‘<fieldname>’) 注: curval()和oldval()仅用于开放式缓冲。
你可能搞不懂curval()返回值和oldval()返回值有什么不一样,若是是在单用户程序中,两者之间是没什么区别,可是若是是在网络中,在开放式锁定下,极可能在本用户编辑一条记录时,另外一个用户也编辑同一条记录,并在本用户"保存"以前进行了"保存",下面是一个例子:
郑某将CONTACTS.DBF指针定位到第2条记录,并点击了"编辑"按钮:
字段
缓冲值 初始值Oldval() 当前值Curval()
姓名
李达 李达 李达
公司名称
狐友技术开发公司 狐友技术开发公司 狐友技术开发公司 而后,郑某将公司名称改成"狐友俱乐部",但尚未保存记录:
字段
缓冲值 初始值Oldval() 当前值Curval()
姓名
李达 李达 李达
公司名称
狐友俱乐部 狐友技术开发公司 狐友技术开发公司 就在此时,于某也将CONTACTS.DBF指针定位到第2条记录,并点击了"编辑"按钮,他改变了公司名称为"猎狐者俱乐部",并保存。此时在郑某的机器上会看到以下结果:
字段
缓冲值 初始值Oldval() 当前值Curval()
姓名
李达 李达 李达
公司名称
狐友俱乐部 狐友技术开发公司 猎狐者俱乐部 注意:在上表中CONTACTS.公司名称、oldval(‘公司名称’)以及curval(‘公司名称’)将返回不一样的值。访问记录中各字段的初始值、缓冲值和当前值,你能够:
l 经过比较缓冲值和初始值来肯定哪些字段被用户修改了;
l 经过比较初始值和当前值来检测在开始编辑后,网络中是否有其它用户修改了同一条记录。 若是你不关心初始值和当前值,而只是但愿检测到某个字段中的内容是否被修改过,能够用getfldstate()函数。这个函数返回一个数值,指出当前记录是否被作了修改。Getfldstate()按如下格式调用:
getfldstate( <fieldName> | <FieldNumber> [ , <Alias> | <WorkArea> ] )
返回值及其意义以下表所示:
返回值
意义
1
没改变
2
字段被编辑或者记录的删除标记被改变
3
添加了一条新记录但没编辑字段,以及记录的删除记录未改变
4
添加了一条新记录并编辑了字段,或者记录的删除标记被改变 记录的删除标记被改变,是指删除记录或恢复(recall)记录。值得注意的是:对记录删除后又立刻进行了恢复,尽管对记录来讲没影响,可是其删除标记被改变过,所以,getfldstate()函数会返回2或4。
若是你没有定义别名或工做区,getfldstate()将对当前打开的表进行操做。将<FieldNumber>定义为0,该函数返回当前记录的添加及删除状态,若是定义为-1,将返回一个字符串,在这个字符串中,第一个数字反映整个表的状态,之后每一个数字返映的是各字段的状态。
以咱们前面讲到的状况为例,在郑某编辑第2条记录时,getfldstate(-1)将返回"112",第一个数字"1"说明记录没有添加或删除,第二个数字"1"说明第一个字段(姓名)没有改变,第三个数字"2"说明第二个字段(公司名称)内容改变了。
缓冲记录的写回
咱们还继续刚才的例子。如今假设郑某点击了"保存"按钮,咱们应该怎样将缓冲中的数据写到记录中(更新表)呢?对于行缓冲来讲,当你移动记录指针或调用tableupdate()函数时,表就会被更新。对于表缓冲来讲,移动记录指针并不会引发表的更新(由于它是多记录被同时缓冲),因此一般状况下只能调用tableupdate()函数来更新表。对于行缓冲最好也用tableupdate()函数,由于这样更好地控制程序的去向。
若是缓冲器中的内容被正确地写入到记录中,tableupdate()返回.T.值,若是记录缓冲没有改变(用户没有编辑任何字段、添加记录或改变记录的删除状态),此时,tableupdate()也返回.T.,尽管实际上什么也没有作。
Tableupdate()能够带几个参数:
Tableupdate( <AllRows>,<Forced>,<Alias>|<Workarea> )
第一个参数指明哪些记录被更新:设为.F.,则只更新当前记录,若为.T.,则更新全部记录(仅影响表缓冲)。若是第二个参数是.T.,那么其它用户的任何修改将被当前用户的修改所覆盖。若是没定义第三个参数,tableupdate()将更新当前表。
怎样取消用户所作的修改呢?对于用内存变量的方法,能够再次用scatter memevar 语句从磁盘上的数据恢复到内存变量中,而对于缓冲来讲,用tablerevert()函数便可达到一样功能。
错误处理
咱们继续"郑某和于某"的例子,当郑某点击"保存"按钮后,代码将执行tableupdate()函数以把缓冲中的数据写入记录。请记住,在郑某编辑记录时于某已经修改了同一条记录并作了保存。当郑某点击"保存"时,tableupdate() 将返回.F.,说明它不能将缓冲写入记录中,为何会是这样呢?
VFP在如下几种状况下没法将缓冲写入记录:
l 当一个用户编辑记录时,其它用户修改并保存了该记录(正如咱们例子中的那种状况)。VFP自动对每一个字段的oldval()值和curval()值进行比较,若是检测到任何不一样,就会产生冲突。
l 用户输入了重复的主索引或候选索引值。
l 违背了某个字段或表的验证规则,或者不支持null的字段出现了null值。
l 某个触发(trigger)失败。
l 其它用户锁定了该记录。
l 其它用户删除了该记录。
当tableupdate()失败时,咱们必须决定下一步作什么,并且,若是你的程序在编辑记录时容许用户点击"下一个"或"上一个"按钮,而这两个按钮中又没有调用tableupdate()的话,你必须得处理在自动保存时将会发生的错误。在这两种状况下,将程序指定到适当的位置就是错误陷阱处理程序。
Visual Foxpro中的多用户及数据缓冲问题(下)
--------------------------------------------------------------------------------
2000-10-6 17:07:00
在VFP中错误处理已经获得改进。之前处理错误陷阱的方法(你仍然能够在VFP中继续使用这些方法)是当错误发生时用on error命令来决定要执行的程序,典型的错误处理程序是查看error()和message()来肯定发生了什么错误,而后采起相应的动做。
如今VFP提供了一种自动的错误处理机制:就是Error方法。若是定义了一个控件或表单中的Error方法,当错误发生时它就被自动执行。aerror()是VFP的一个新增函数,经过传递一个参数,该函数能够建立或更新一个含有如下元素的数组
元素
类型 描述
1
数字 错误号(与error()相同)
2
字符 错误信息(与message()相同)
3
字符 若是有一个错误信息参数(与sys(2018)相同),则返回之(例如:一个字段名),无,则返回.NULL.
4
数字或字符 发生错误的工做区。若是没有,则返回.NULL.
5
数字或字符 若是一个触发器失败,返回触发器号(插入为1,更新为2,删除为3),若是没有则返回.NULL.
6
数字或字符 .NULL.(应用于OLE和ODBC错误)
7
数字 .NULL.(应用于OLE错误) 例如:aerror(IaERROR)会建立或更新一个称为IaERROR的数组。
如下是VFP在将缓冲写入表时可能发生的一些错误:
错误号#
错误信息 说明
109
记录正由其它用户使用 1539 触发器失败 检测数组的第5个元素能够肯定是哪一个触发器失败了
1581
字段不接受空值(null) 检测数组的第3个元素能够肯定是哪一个字段引发的错误
1582
违反了字段的验证规则 检测数组的第3个元素能够肯定是哪一个字段引发的错误
1583
违反了记录的验证规则 1585 记录已被其它用户修改 1884 违反了索引的惟一性 检测数组的第3个元素能够肯定是哪一个索引标记引发的错误 对于以上这些错误,大部分能够直接处理:提示用户问题所在,而后让用户在"编辑"状态下改正错误或是取消操做。对于#1585错误(记录已被其它用户修改),有如下几种处理错误的方法:
l 能够提示当前用户有别人修改了该记录,而后用tablerevert()取消当前用户的编辑内容。我想多数人对这种方法会不高兴的。
l 能够用tableupdate(.F. , .T.)来强行更新记录,使当前用户的修改覆盖其它用户修改。这样作,当前用户天然是高兴的,但其它用户可能就要不满了。:(
l 复制一个相同的表单(在VFP中对同一表单建立多个实例是很是容易的),在上面显示出其它用户对该记录的修改,这样,当前用户就能够决定是保存仍是不保存其它用户的修改,也就是说,能够用tableupdate(.F. , .T.)强行更新或是用tablerevert()来取消编辑。
有更好的方案来检测咱们是否遇到了"真正的"冲突,所谓"真正的"冲突,是指两个用户在同一时间修改了同一个字段。若是他们修改的是同一记录的两个不一样字段,咱们能够只更新当前用户修改的字段,而保留另外一个用户修改的字段,使之不受影响。例如,在一个订单处理系统中,一个用户修改了产品介绍,而同时另外一个用户在对该产品下订单,正在输入数量,这两个修改相互独立,并无冲突,这时咱们不要一次更新整条记录,而是只更新本身修改的那个字段,如此一来,两个用户都会感到满意。
如下是实现这一想法的思路:
l 查找oldval()与curval()不一样的字段,若是有,说明该字段已被其它用户编辑过,若是该字段的缓冲值与oldval()相同,说明当前用户并无修改该字段。这种状况下,咱们能够将curval()中的值先传给缓冲值,再更新,这样就能够避免缓冲值覆盖新值了。
l 查找缓冲值与oldval()不一样的字段,这些字段是当前用户修改过的字段,若是oldval()又等于curval(),说明其它用户没有改动过该字段,这种状况下,咱们能够放心地覆盖掉它。
l 若是咱们找到的字段,其缓冲值与oldval()不一样,但与curval()相同,这说明两个用户对同一字段作了相同的修改。这种状况看起来好象不大可能,实际上是会发生的。好比有人发来了一个公司的地址变更信息,而偏偏有两个用户同时决定据此来更新记录中的公司地址。由于他们所作的修改内容是相同的,咱们能够覆盖另外一个便可。可是,若是是以同一个数量更新一个量值的话(好比两人同时下订单,且输入了相同的数量),你就不能简单地覆盖字段,应该将此看做为"真正的"冲突。
l 若是发现一个字段的缓冲值既不一样于oldval()也不一样于curval(),并且oldval()与curval()也各不相同,这说明两个用户都修改了同一个字段,且值不相同。这种状况才是咱们遇到的真正的冲突,咱们不得不决定怎样处理这一冲突。
在手工输入存货数量或账目余额时,有一种可能就是其它用户输入的值对缓冲值会产生影响。好比,若是检测到oldval()是10, curval()是20,说明其它用户将数额已增长了10;若是如今缓冲值为5,说明当前用户要将数额在原基础(10)上减小5,所以新的缓冲值就应该是value+ oldval()- curval(),即等于15,应该用此数来更新字段。
对于日期型字段的冲突,就要考虑商业规则或按实际状况处理。例如,在"病人预定时间"程序中,有一个字段保存着病人下次预定医生的时间,若是该字段两个日期发生冲突的话,那么,靠近当前日期的那个日期极可能是正确的。固然,如果其中一个预定日期比当前日期还要早的话(已通过期),那就应该取靠后的那个日期了。
其它类型的字段,特别是字符型和备注型字段,一般状况下若是不询问用户以决定是覆盖其它用户的修改仍是取消本身的修改的话,则冲突很差解决。只有当用户在屏幕上看到其它用户到底作了什么修改后,他才能作出正确地判断。
如下是解决冲突的一些代码(这些代码假设已检测到错误码为#1585,即记录已被其它用户修改过。)
* 检测每个字段,看哪一个发生了冲突。
llConflict = .F.
for lnI = 1 to fcount()
lcField = field(lnI)
llOtherUser = oldval(lcField) <> curval(lcField)
llThisUser = evaluate(lcField) <> oldval(lcField)
llSameChange = evaluate(lcField) == curval(lcField)
do case
* 其它用户编辑了该字段,而当前用户没编辑,因此直接用新值便可。
case llOtherUser and not llThisUser
replace (lcField) with curval(lcField)
* 其它用户没有编辑该字段,或者两者作了相同的修改,所以咱们无需作任何处理。
case not llOtherUser or llSameChange
* 两个用户以不一样的值修改了该字段。
otherwise
llConflict = .T.
endcase
next lnI
* 若是发生了冲突,处理之!
if llConflict
lnChoice = messagebox(''''Another user also changed this '''' + ;
''''record. Do you want to overwrite their changes (Yes), '''' + ;
''''not overwrite but see their changes (No), or cancel '''' + ;
''''your changes (Cancel)?'''', 3 + 16, ''''Problem Saving Record!'''')
do case
* 覆盖其它用户的修改。
case lnChoice = 6
= tableupdate(.F., .T.)
* 经过产生一个表单实例来查看其它用户的修改内容。
case lnChoice = 7
do form MYFORM name oName
* 取消当前用户的修改。
otherwise
= tablerevert()
endcase
* 若是没有发生冲突,则强行更新。
else
= tableupdate(.F., .T.)
endif llConflict
表缓冲的写入
咱们前面已经讲到,能够用tableupdate(.T.)将表缓冲中的全部记录一次写入磁盘。与行缓冲同样,若是其它用户修改了表(或是其它什么出错缘由)而不能正确更新表,tableupdate(.T.)将返回.F.值。
前文所讲的错误处理程序在行缓冲模式下运行良好,由于咱们在某一时刻只关心单条记录。但对于表缓冲来讲,咱们不得不考虑每一条记录,由于在缓冲区中可能既有修改过的记录,也有未修改过的记录,咱们怎样知道到底更新哪条记录呢?若是用tableupdate(.T.)失败(返回.F.),状况就变得更加复杂化:咱们不知道错在哪条记录上!并且有些记录可能被作过"保存",因此还不止一条记录会发生冲突呢。请不要着急:),VFP新增函数getnextmodified()能够精确地告诉咱们想知道的信息:该函数返回下一个被修改记录的记录号。若是返回值为0,说明在缓冲区中没有被修改过的记录。这个函数接收两个参数:第一个参数是一个记录号,正是从这个记录号开始向下查找下一个被修改的记录;第二个参数是查找的工做区别名。最被,你应该将0传给第一个参数,这样getnextmodified()就会找到第一个被修改的记录,若要继续找下一个被修改的记录,只要将当前记录的记录号传给第一个参数便可。
下面是在刚才处理冲突的程序基础上改进后的代码,它用来处理表缓冲更新失败时的操做。
* 先找到第一个被修改过的记录。
lnChanged = getnextmodified(0)
do while lnChanged <> 0
* 移动记录指针并尝试锁定它。
go lnChanged
if rlock()
* 检测每个字段,看哪一个发生了冲突。
llConflict = .F.
for lnI = 1 to fcount()
lcField = field(lnI)
llOtherUser = oldval(lcField) <> curval(lcField)
llThisUser = evaluate(lcField) <> oldval(lcField)
llSameChange = evaluate(lcField) == curval(lcField)
do case
* 其它用户编辑了该字段,而当前用户没编辑,因此直接用新值便可。
case llOtherUser and not llThisUser
replace (lcField) with curval(lcField)
* 其它用户没有编辑该字段,或者两者作了相同的修改,所以咱们无需作任何处理。
case not llOtherUser or llSameChange
* 两个用户以不一样的值修改了该字段。
otherwise
llConflict = .T.
endcase
next lnI
* 若是发生了字段冲突,咱们能够在此处理它,与行缓冲不一样的是,咱们也能够如今不处理,由于之后全部记录将被写入,到时会处理的。
if llConflict
lnChoice = messagebox(''''Another user also changed '''' + ;
''''record '''' + ltrim(str(lnChanged)) + ''''. Do you want to '''' + ;
''''overwrite their changes (Yes), not overwrite but see '''' + ;
''''their changes (No), or cancel your changes (Cancel)?'''', 3 + 16, ;
''''Problem Saving Record!'''')
do case
* 若是选择了覆盖其它用户的修改,在此能够不作处理,由于之后将一次性更新。
case lnChoice = 6
*经过产生一个表单实例来查看其它用户的修改内容。
&nb
|