数据库

本类阅读TOP10

·SQL语句导入导出大全
·SQL Server日期计算
·SQL语句导入导出大全
·SQL to Excel 的应用
·Oracle中password file的作用及说明
·MS SQLServer OLEDB分布式事务无法启动的一般解决方案
·sqlserver2000数据库置疑的解决方法
·一个比较实用的大数据量分页存储过程
·如何在正运行 SQL Server 7.0 的服务器之间传输登录和密码
·SQL中两台服务器间使用连接服务器

分类导航
VC语言Delphi
VB语言ASP
PerlJava
Script数据库
其他语言游戏开发
文件格式网站制作
软件工程.NET开发
Visual Foxpro中的多用户及数据缓冲问题

作者:未知 来源:月光软件站 加入时间:2005-5-13 月光软件站

主题:Visual Foxpro中的多用户及数据缓冲问题

绍 

很多狐友(Foxers)都是从Dbase―FoxBase―Foxpro―VFP这样一条路走过来的,如果说从FoxBase Foxpro是一次飞跃,那么从FoxproVFP就是一次升华。漫漫编程路上的两次大变化都伴随着升级的兴奋与适应的痛苦,惯性思维往往使我们容易忽略新版本的新内容。我们先来看看下面这个在表单中编辑记录的例子:

Foxpro 2.X时代,我是这样设计的:

1、   在屏幕上放置与表中字段对应的文本框(TEXT)控件,用来存放相应的内存变量(如M.CUST_IDTM.NAME等);

2、   当用户将表的指针定位到某一特定记录时(比如按了"下一个记录"按钮),就用scatter memvar 语句把该记录的所有字段传给相应的内存变量,再用show get来刷新屏幕上的文本框中显示的值。在这一时刻,用户是不能编辑这些变量的(此时文本框或者被设置为不可用(Disabled),或者其when子句返回的是.F.值),因为用户处在"浏览"状态。

3、   当用户选择了"编辑"按钮时,程序就锁定该记录(如果无法锁定,则显示提示信息),然后检测每个字段值和相应的内存变量值(如果有不同的,说明一定是有其它用户在我们进入编辑状态后修改并保存了记录,在这种情况下,要显示提示信息),之后再用scatter memvarshow get语句刷新,这样用户就看到记录中的当前值了;

4、   如果所有字段和相应的内存变量完全匹配,就把变量所在的文本框控件都设置为可用(Enabled)或都让when子句返回.T.,以便让用户可以来编辑这些变量;

5、   当用户选择了"保存"按钮时,根据一定的规则对输入的数据进行检验,然后用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())

使用缓冲

缺省情况下缓冲器是关闭的,这种情况下在更新表时VFPFoxpro 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()函数会返回24

如果你没有定义别名或工作区,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.
(应用于OLEODBC错误)

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

转载!

 




相关文章

相关软件