打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Visual FoxPro 中的错误处理
Visual FoxPro 中的错误处理

Doug Hennig ,Interpret by R.M.H
简介
与FoxPro 2.x相比,Visual FoxPro对错误的处理更为灵活但也更为复杂。当对象具有Error方法来处理局部错误时,怎样为你的应用程序提供公共的、全局错误处理服务?当发生错误时如何恢复?这里提供一种经证明是行之有效的方法来实现Visual FoxPro应用程序的错误处理 – 开始于单独的控件,结束于一个全局的错误处理对象。
错误处理基础
在错误处理中有许多困难的问题:设置错误处理器,检查错误的情况,提示用户发生了什么情况(并可能将其写入一个文件供以后分析),并解决问题(试着再次执行命令,继续出错的语句的下一条语句,退出系统等等)。
设置错误处理
与FoxPro 2.x中的设置错误处理相同,VFP中要设置全局错误处理仍然使用on error命令。举例如下:

on error do ERR_PROC with error(), sys(16), lineno()

这些参数告诉错误处理程序:错误号,发生错误的程序的名字,行号。你可以按你的需要传递任意参数到错误处理程序。
VFP以各对象的ERROR事件的方式,提供了对全局错误处理的能力。在VFP中每一个对象的事件模块都具有Error事件。当然,并非每一个对象都有Error方法。如果你不清楚这种差别,记住,事件是被用户或系统的某些动作触发的(击键,鼠标单击,或者一些Visual FoxPro认为是错误的东西),当事件发生时方法编码被执行。当一个消息传递到一个对象通知它执行方法时,方法代码也会被执行。在很多事件中,如鼠标单击,如果对象的方法中没有代码,事件被忽略或执行默认的动作。可是,当错误出现时,将会发生什么取决于一系列的事情。
当一个对象调用另一对象或一个非对象程序(如PRG文件)时出现错误、且该对象存在Error方法时,对象的Error方法将被调用。如果该对象没有Error方法将会发生什么情况呢?当我第一次使用VFP时,我假设该对象的父容器(如Form)的Error方法将被调用。然而,情况并非如此。实际情况是,如果任何在调用堆栈上的对象存在着Error方法,该方法将被调用;如果没有,on error例程(如果有的话)被调用。如果没有on error例程,Visual FoxPro执行它自己的错误处理(就是那个声名狼籍的取消/忽略对话框),然后程序崩溃。
检查错误情况
FoxPro 2.x中的一系列的函数和VFP都有助于检查错误发生的原因,包括error(),message(),lineno(),sys(16),和sys(2018)。VFP还提供一个aerror()函数,用于提供最近发生的错误的信息并将这些信息存入一个数组。虽然该数组中的一些信息也可从其它函数获得,对于某些类型的错误(如OLE,OBDC,和触发器),aerror()其它函数则不能得到错误信息(例如何种触发器造成了错误)。
提示用户发生了什么
坦率地告诉用户发生了错误,并允许用户决定如何处理错误。主要涉及到要告诉用户什么、并提供给用户什么样的选择。对要显示的错误信息进行适当修改,并以尽可能平静的方式措词,以防止惊慌失措的用户同时按下“Ctrl-Alt-Delete”。它也应提供如何处理错误的信息。例如,一些简单的错误如打印机未联接可以询问用户确信打印机正确地联接并装好了纸。这样用户可以在打印和取消打印之间作出选择。如果一个用户试图修改一个被别的用户锁定的记录,你可以告诉用户当前正有另一用户在修改该记录,并给出再试一次或取消修改的选择。
在VFP中可能出现的错误多达600种以上,你可能会令人心悸地想为每一个错误给出有意义的信息。幸运的是,如果你查看VFP的帮助中关于错误信息的主题,你会发现它们中的大多数仅归类于下列少数的种类:
· 决不会发生
· 如果程序员在写程序前未喝酒,就不会发生
· 一个以上的用户同时使用该系统时会发生
· 需要个别处理的错误
在第一种类别中,你的程序除了退出之外没有别的选择。第二种错误可以在测试中被发现,但若未发现,你的应用程序将报告并登录该错误,然后关闭。第三种类型的错误也可以在测试中被发现,但是仅出现一个简单的类似于“你现在不能操作”的错误信息。真正需要你处理的只是第四种类型的错误。这种类型错误的例子是字段或表验证规则失败,主关键字失败(如果你使用了系统指定的代理关键字,这种情况很少发生),触发器失败,和文件未找到。
在显示错误信息给用户之前,许多开发者喜欢登录错误到一个错误登录文件。这已被证明了是极有价值的,因为用户常常会含糊的向你报告错误发生的细节。错误登录文件可以是一个文本文件,但是我使用一个表中的字段来保存错误发生的日期和时间(可以是VFP中的DateTime字段),用户名,错误号和信息,行号和错误出现处的代码,一个备注型字段用于包含当前的内存变量。
解决错误
解决错误是复杂的事情。retry命令试着再次执行发生错误的命令,但是对于很多错误而言,由于用户自己处理错误的能力所限,该命令对用户处理错误没有多大帮助。return则继续执行发生错误的命令的后面一行程序,但由于造成错误的程序行并没有执行,所以因此而造成的二级错误信息可能会接踵而至,如变量没有建立(毕竟,如果可以忽略造成错误的程序且不会带来任何问题的话,它是做什么用的呢?)。cancel不是一种现实的选择,因为它终止应用程序,这样就没有机会来适当地进行退出前的系统清理。以被约束的方式关闭应用程序是一个正确的选项,因为很多错误是由于编程人员或操作系统造成的,在问题被解决前没有更多的选择。作为一个开发者,你可能也愿意选择取消应用程序并返回到命令窗口。这样做的代码将尽可能地清除环境。
另一个选项是使用return to master或return to来退出发生错误的程序而返回到主控程序或指定的程序。要小心处理这种选项,因为这样会使系统进入一种混乱状态:表单和窗口仍然存在,表或游标任然是打开的,等等。在使用return to命令前,尽可能地清除这些东西是一个好的办法。
设计错误处理方案
全局Error 处理和对象的Error方法代表了一个事物的两个方面:
· 当Error方法是造成错误的对象的一部分时,全局错误处理器与发生错误的根源相距较远。这样,Error方法知道它自己所处的环境,那些错误可能会发生,如何解决错误。例如,CommonDialogs ActiveX控件(显示文件,打印,色彩和打印机的对话框)在用户选择取消时,将会造致错误。它会无声地让全局错误处理器处理错误,由于它只知道那是一个某种类型的OLE错误并不知道怎样去处理它。只能在CommonDialogs控件的Error方法中放入代码来处理用户选择取消的情况。
· 全局错误处理器在VFP的事件处理器之外,用FoxPro的老的“ON”事件方法进行调用(该方案仍被用于菜单和on key label命令)。这意味着,你不能使用象Thisform一样的对象语法,或发布象private datasessions这样的会使错误处理变得复杂的命令。
· 全局错误处理器可以有效的把错误处理服务放入同一个地方(如错误登录和显示)。但在你的应用程序中的每一个对象的Error方法中,它处理多种不可预期的错误时确是低效率的(如网络连接断线)。
让我们看看设计一个将两个世界进行最好的一体化的错误处理方案。我们希望以尽可能有效的方式处理错误,同时还提供个别对象处理它们自己特殊错误的能力。这里是我们使用的策略:
· 就象一个玩笑中所说的那样,一个对象比其父类更清楚它正在做什么,因此一个对象的Error方法将处理它自己所能处理的一切错误。并将它所不能处理的部分用dodefault()命令传递给它的父类。在该类层次中的各个子类都这样做。如果一个子类或一个类的实例不需要处理任何特殊的错误,则其Error方法中不会放置任何代码,以促成其父类代码自动执行。这样,SFDeepSubClassTextBox.Error调用SFSubClassTextBox.Error再调用SFTextBox.Error。
· 对象的最顶层父类的Error方法将处理任何它能处理的错误。它用This.Parent.Error传递那些经不能处理错误到它的容器。
· 由于容器类与控件一样工作(它们传递不可处理的错误到它们的父类,最顶层父类传递错误到它们的容器类),实际效果是我们上移一个类层次而进入到容器类的错误处理器。
· 最顶层父类的最外部容器的Error方法将处理它可处理的任何错误。它会传递那些它不能处理的错误到全局错误处理器。
以下是一个设计流程图:在链中的每一个对象处理错误或将它传递到链中的下一个对象。在这个多层方案中,当你从对象到全局错误处理器时,错误处理获取较少的细节和较多的一般信息,允许错误在各个不同等级进行处理。图 1 表明了这种策略。

图 1. 错误处理策略。
我们在应用程序启动时从SFErrorMgr实例化一个对象到一个全局变量oError。它的一个方法(ErrorHandler)既可以象上述一样被对象直接调用也可以用on error命令调用。错误处理对象有一个简单的接口(对编程而言),因此SFErrorMgr接受与Error方法相同的参数(错误号,方法名,行号)并返回一个说明用户选择的串给用户(或对象)以便处理错误。错误对象处于处理链的末端,因此它不知道调用它的地方的环境(可能经历了一系列的传递,在不同的数据工作期中等)。作为一个结果,它不能真正地作更多的“处理”(应该是:解决)。它的用途是显示一信息给用户,登录错误以备后用,并决定下一步的行动(在可靠的可预知条件下)或更多的,可靠的,将怎样做的询问。这样,错误处理对象将真正地用于处理你没有预料到的可预知的错误(一但它们出现,你会修改造成错误的对象,类,或过程)和不可预知错误(真正的bugs或未预料的环境条件)。
全局错误处理器自己会作出一个全局性的决定(调出VFP的Debugger或关闭应用程序)或允许对象引发错误来作出最后的决定。要允许后者,在错误处理链中的每一步返回一个决定码到上一级。为简便起见,我决定返回一个串来说明用户选择了那一决定:“重试”重新执行引起错误的命令,“继续”则从引发错误的程序的下一行继续运行程序,或“关闭表单”用于关闭当前起控制作用的表单。各对象依返回的信息执行相应的行动。由于容器类的Error方法可能已经被其成员对象或它自己的某个方法调用,容器类必须决定是向上传递返回信息还是由它自己处理。在稍后,我们将会看见这些代码。
该方案有一个问题:控件置于VFP的Page,Column,或其它没有Error方法代码的容器基类上时,由于它们调用一个空的方法,所以它们实质上没有错误捕捉!解决办法是在类层次中向上移动直到找到一个具有Error方法代码的父类。如果我们不能找到这样的父类,则显示一个一般性的错误信息(这并不可靠,由于所有的表单类都是以SFForm类为基类,而SFForm没有Error方法代码)。
要注意的事是完整的错误处理链必须是你的应用程序中最没有Bug的(经彻底调试的)部份,因为在错误状态下再次出现错误时,唯一可依靠的就是取消/忽略对话框了。幸运的是,由于你可以将错误处理代码放入你的大多数工作框架中,一但你使它们工作,它们不会受到更多的关注(虽然与众不同的环境条件仍会使错误处理器自身失败)。不要试着在错误处理对象中建立一个错误处理方法:当错误处理器自身出错时,它不会被调用。
Error方法
让我们注意错误处理策略的更多的细节。当一个对象发生错误时开始点是在该对象所具有的Error方法,因此我们从这里开始。
在我的应用程序的基类中,大多数类的Error方法代码,包含在SFCTRLS.VCX中,清单如下(包括ccMSG_RETRY定义在ERRORS.H中,包含于SFCTRLS.H中,每一个类的include文件)。我说“大多数类”,是因为顶级容器如form和toolbar的工作方式有一小点不同。这是我的少数的希望VFP支持多重继承的理由之一;在这和情况下,你需要象在VB中那样,将相同的代码放入所有类的Error方法中(采用复制-粘贴的方法)。

lparameters tnError, ;
tcMethod, ;
tnLine
local loParent, ;
lcMethod, ;
lcReturn, ;
lcError

* 向上移动一个层次直到我们找到一个具有Error 方法的父类

if type('Thisform') = 'O'
loParent = iif(pemstatus(Thisform, 'FindErrorHandler', ;
5), Thisform.FindErrorHandler(This), .NULL.)
else
loParent = .NULL.
endif type('Thisform') = 'O'
lcMethod = This.Name + '.' + tcMethod
do case

* 我们有一个可以处理错误的父类。

case not isnull(loParent)
lcReturn = loParent.Error(tnError, lcMethod, tnLine)

* 我们有一个错误处理对象, 因此调用它的 ErrorHandler()方法。

case type('oError') = 'O' and not isnull(oError)
lcReturn = oError.ErrorHandler(tnError, lcMethod, ;
tnLine)

* 一个全局错误处理器在起作用, 因此让我们传递错误信息给它。
* 以适当的值替换可靠的参数以传递到错误处理器(程序名, 错误号以及行号)。

case not empty(on('ERROR'))
lcError = strtran(strtran(strtran(strtran(upper(on('ERROR')), ;
'SYS(16)', lcMethod), ;
'PROGRAM()', lcMethod), ;
'ERROR()', 'tnError'), ;
'LINENO()', 'tnLine')

* 如果错误处理器是以 DO 命令调用, 进行宏扩展并假设返回值是"继续"。如果错误处理器是
* 被一个函数调用 (比如一个对象的方法), 调用它并获得返回值。

if left(lcError, 3) = 'DO '
&lcError
lcReturn = ccMSG_CONTINUE
else
lcReturn = &lcError
endif left(lcError, 3) = 'DO '

* 显示一个一般的对话框。

otherwise
messagebox('错误号' + ltrim(str(tnError)) + ;
'对象 ' + This.Name + ;
'方法 '+ tcMethod + ;
'行号 '+ ltrim(str(tnLine)), 0, ;
_VFP.Caption)
endcase

* 确信返回信息是可接受的。否则, 假定为
* "继续"。

lcReturn = iif(type('lcReturn') <> 'C' or ;
empty(lcReturn) or not lcReturn $ ccMSG_CONTINUE + ;
ccMSG_RETRY + ccMSG_CANCEL, ccMSG_CONTINUE, lcReturn)

* 处理返回值。

do case
case '.' $ tcMethod
return lcReturn
case lcReturn = ccMSG_RETRY
retry
case lcReturn = ccMSG_CANCEL
cancel
otherwise
return
endcase
以上代码检查表单控件是否具有FindErrorHandler方法,如果有,调用它来定位到控件的第一个父容器(不要被这个代码迷惑;你可以在供提供的源代码中查看它们)。这样做防止了这样一种情形:由于Page,Column,或其它容器类没有Error方法代码而致使错误停留在这些类中。如果一个可进行错误处理的父容器找到了,它的Error方法以相同的参数被调用,除非对象的名字被加到了tcMethod,因此我们的错误处理服务程序能够知道错误是发生在那一对象。如果父容器未找到但存在一个全局错误处理器(稍后我们再谈全局错误处理程序),它的ErrorHandler方法被调用。如果一个ON ERROR例程存在,我们将调用它(首先将我们知道的值调整为它可接受的参数),无论它是一个函数还是一个过程。如果我们没有东西可传递,就使用messagebox()来显示一条信息。
错误处理器返回的值将决定怎样处理错误。首先,如果该错误不是发生在本地,我们必须返回决定信息而不是自行处理。我们将用检查发生错误的方法的名字的句点号(.)来检查这一点;如果错误出现在一个类的方法中,VFP只传递方法名,但对象成员传递对象和方法名,这就提供了一种快速的方法用对象自己或对象名来区别错误原因。如果不是这种情况,则是我们的错误,因此我们将‘重试’,‘取消’,或‘返回’。
由于它们是“顶级”容器(我没有使用表单集Formsets),SFForm和SFToolbar类的Error方法与其它对象不同。该方法使用自定义的SetError方法来组织一些自定义属性形成关于错误的信息,并且用HandleError方法来处理错误。最后它处理返回值,它自己要么执行一个行动(比如关闭表单)要么返回到调用这个方法的对象。注意如果对象是DataEnvironment且它自己替换处理这些错误,它将不返回值。你可能想改变这一行为。同时也应注意使用return to;稍后我们将讨论这一问题的更多细节。

lparameters tnError, ;
tcMethod, ;
tnLine
local lcReturn,
lcReturnToOnCancel

* 使用 SetError() 和 HandleError() 来取得错误信息并处理它。

with This
.SetError(tnError, tcMethod, tnLine)
lcReturn = .HandleError()

* 如果用户选择了”取消”,指明到那里去。

do case
case type('oError') = 'O' and not isnull(oError)
lcReturnToOnCancel = oError.cReturnToOnCancel
case type('.oError') = 'O' and not ;
isnull(.oError)
lcReturnToOnCancel = .oError.cReturnToOnCancel
otherwise
lcReturnToOnCancel = 'MASTER'
endcase
endwith

* 处理返回值, 取决于错误是 "ours" 或来自一个成员.

do case
case lcReturn = ccMSG_CLOSEFORM
This.Release()
return to &lcReturnToOnCancel
case '.' $ tcMethod and ;
not 'DATAENVIRONMENT' $ upper(tcMethod)
return lcReturn
case lcReturn = ccMSG_RETRY
retry
case lcReturn = ccMSG_CANCEL
return to &lcReturnToOnCancel
otherwise
return
endcase
我们不能仅着眼于SetError 方法(你可以在提供的源代码中自己检验这一点) 但以下是HandleError 方法代码:
local lnError, ;
lcMethod, ;
lnLine, ;
loError, ;
lcMessage, ;
lcReturn
with This
lnError = .aErrorInfo[.nLastError, cnAERR_NUMBER]
lcMethod = .Name + '.' + ;
.aErrorInfo[.nLastError, cnAERR_METHOD]
lnLine = .aErrorInfo[.nLastError, cnAERR_LINE]

* 如果存在着错误处理对象,取得一个引用到我们的错误处理对象。
* 它可能是表单号也可能是一个全局对象。

do case
case type('.oError') = 'O' and not isnull(.oError)
loError = .oError
case type('oError') = 'O' and not isnull(oError)
loError = oError
otherwise
loError = .NULL.
endcase
lcMessage = ccMSG_ERROR_NUM + ltrim(str(lnError)) + ;
ccCR + ccMSG_MESSAGE + ;
.aErrorInfo[.nLastError, cnAERR_MESSAGE] + ccCR + ;
iif(empty(.aErrorInfo[.nLastError, cnAERR_SOURCE]), ;
'', ccMSG_CODE + ;
.aErrorInfo[.nLastError, cnAERR_SOURCE] + ccCR) + ;
iif(lnLine = 0, '', ccMSG_LINE_NUM + ;
ltrim(str(lnLine)) + ccCR) + ccMSG_METHOD + lcMethod
do case

* 存在着一个错误处理对象, 因此调用它的
* ErrorHandler() 方法。

case not isnull(loError)
lcReturn = loError.ErrorHandler(lnError, lcMethod, ;
lnLine)

* 一个全局错误处理程序正在起作用, 因此我们将错误传递给它。以适当的
* 值为错误处理程序替换可靠的参数(程序名, 错误号,和行号)。

case not empty(on('ERROR'))
lcError = strtran(strtran(strtran(strtran(upper(on('ERROR')), ;
'SYS(16)', lcMethod), ;
'PROGRAM()', lcMethod), ;
'ERROR()', 'tnError'), ;
'LINENO()', 'tnLine')

* If the error handler is called with DO, macro expand it
* and assume the return value is "CONTINUE"。If the error
* handler is called as a function (such as an object
* method), call it and grab the return value if there is
* one。

if left(lcError, 3) = 'DO '
&lcError
lcReturn = ccMSG_CONTINUE
else
lcReturn = &lcError
endif left(lcError, 3) = 'DO '

* We don't have an error handling object, so display a
* dialog box。

otherwise
lcReturn = iif(messagebox(lcMessage, ;
MB_ICONEXCLAMATION + MB_OKCANCEL, ;
_screen.Caption) = IDCANCEL, ccMSG_CANCEL, ;
ccMSG_CONTINUE)
endcase
endwith
lcReturn = iif(type('lcReturn') <> 'C' or empty(lcReturn) or ;
not lcReturn $ ccMSG_CONTINUE + ccMSG_RETRY + ccMSG_CANCEL + ;
ccMSG_CLOSEFORM, ccMSG_CONTINUE, lcReturn)
return lcReturn

HandleError试着通过一个全局错误变量oError或表单的oError属性传递错误到全局错误处理对象。该方案允许你在可能的情况下,拥有一个定制版本的与表单关联的全局错误处理器。如果一个ON ERROR例程存在,我们调用它(首先将我们知道的值调整为它可接受的参数),无论它是一个函数还是一个过程。如果我们没有东西可传递,就使用messagebox()来显示一条信息。然后从错误处理器返回的值回传到Error方法。
全局错误处理
SFErrorMgr是一个派生于SFCustom的非可视类。它包含在SFMGRS.VCX中并使用SFERRORMGR.H包含文件中定义的一些常量。在应用程序启动时,它被实例进全局变量oError中(参见SYSMAIN.PRG)。我们不必查看该类的全部源代码,只需查看对我们的错误处理方案有关的方法代码。你自己可已查看任何其它的方法。
Init方法接受三个参数:当错误出现时显示给用户的对话框的标题(保存于cTitle属性中),一个指明Init是否将保存当前的on error处理器并改变它到它自己的ErrorHandler方法,以及类实例化为对象时的名称(这对于on error命令是必需的,因为我们不能使用类名词This)。
一般说来,Destroy方法会清除已被类改变了的东西;在这种情况下,它重置VFP的错误处理程序到对象被实例化以前。
ErrorHandler方法被错误处理链中最后的对象直接调用,并由于它也是on error错误处理器故也被间接调用。以下是该方法的代码:
lparameters tnError, ;
tcMethod, ;
tnLine
local lcCurrTalk, ;
lcChoice, ;
lcProgram
with This

* Ensure TALK is off。

if set('TALK') = 'ON'
set talk off
lcCurrTalk = 'ON'
else
lcCurrTalk = 'OFF'
endif set('TALK') = 'ON'

* Put the error into the aErrorInfo array and set the
* lErrorOccurred flag。

.GetErrorInfo(tcMethod, tnLine)

* If errors aren't being suppressed, display the error
* and get the user's choice of action。

lcChoice = ccMSG_CONTINUE
if not .lSuppressErrors

* Log the error if necessary。

if .lLogErrors
.LogError()
endif .lLogErrors

* Display the error and get the user's choice if desired。

if .lDisplayErrors
lcChoice = .DisplayError()
do case

* Cancel or Quit in development environment: remove any
* WAIT window, revert all open cursors and issue a CLEAR
* EVENTS (in the case of Quit), and then return to the
* top-level program。

case lcChoice = ccMSG_CANCEL or ;
(lcChoice = ccMSG_QUIT and version(2) <> 0)
wait clear
if lcChoice = ccMSG_QUIT
.lQuit = .T.
.RevertAllTables()
clear events
endif lcChoice = ccMSG_QUIT
lcProgram = .cReturnToOnCancel
return to &lcProgram

* Display the debugger (development environment): activate
* the Trace and Debug windows。

case lcChoice = ccMSG_DEBUG and version(2) <> 0
activate window debug
set step on

* Retry programmatic code: we must do the retry here,
* since nothing will receive the RETRY message (as is the
* case with an object)。

case lcChoice = ccMSG_RETRY
lcMethod = upper(tcMethod)
if at('.', lcMethod) = 0 or ;
inlist(right(lcMethod, 4), '.FXP', '.PRG', ;
'.MPR', '.MPX')
if lcCurrTalk = 'ON'
set talk on
endif lcCurrTalk = 'ON'
retry
endif at('.', lcMethod) = 0 ...

* Quit: revert all open cursors, then quit。

case lcChoice = ccMSG_QUIT
.lQuit = .T.
.RevertAllTables()
on shutdown
quit
endcase
endif .lDisplayErrors
endif not .lSuppressErrors

* Restore TALK。

if lcCurrTalk = 'ON'
set talk on
endif lcCurrTalk = 'ON'
endwith
return lcChoice
当错误发生时,三个参数传递到ErrorHandler:错误号,发生错误的程序名,发生错误的行号。ErrorHandler使用GetErrorInfo方法来设置lErrorOccurred属性为.T.并设置关于错误的信息到aErrorInfo属性。如果lSuppressErrors属性值为.T.,不登录错误且不显示错误信息(这用于当你想捕捉错误,但又不想登录和显示错误信息给用户时)。否则,LogError方法被调用来登录错误并且DisplayError方法用于显示关于错误的信息并取得用户的选择。这些选择是:
· 调试:该选项仅当lShowDebug设置为.T.且我们是运行于VFP的开发版中时可选,调出跟踪和调试窗口。仅当开发者使用时,lShowDebug被设置为.T.(这一点可以在用户表中或Windows注册表中检查到)。
· 继续:返回到造成错误的程序的后面一行,并继续执行程序。
· 重试:重新执行出错的程序。这里使用了一小点技巧:如果ErrorHandler已经被明确地调用了(就是说,从一个对象的Error方法),则不能直接发布retry命令,由于那样做会把控制返回到调用它的方法,而不是造成错误的方法。在这种情况下,我们将仅返回一个信息“重试”。然而,如果错误出现在程序文件中(PRG或MPR文件中),ErrorHandler将以on error方法被调用,因此只返回该信息还不够。在这种情况下,ErrorHandler必须自己直接发布retry命令。
· 取消:在CompuServe上,一个常见的问题是:如何避免程序或方法中的其余代码在执行时造成错误?你不能使用cancel,因为那样取消了整个应用程序。return返回到同一方法中因此那样做并无好处。解决办法是返回到到你的应用程序的read events语句所在的程序(在一般情况下,当你的方法代码尚未执行时,你就发布了该命令)。由于该过程可能不是你的应用程序的第一个执行的程序,我们将控制返回到属性cReturnToOnCancel中指定的程序中要比使用return to master要好。当你实例化SFErrorMgr时,你可以适当的设置该属性的值(你可以设置它的值为“MASTER”如果应用程序的第一个过程包含了read events)。如果你的read events语句是在一个对象的方法中,设置为该对象的名字;例如,如果你的read events语句是在oApp.EventHandler中,把“EventHander”放入cReturnToOnCancel属性中。
· 退出:退出应用程序。根据我们是否使用的是VFP的开发版,而可能不同的需求。在开发模式下,当你不得不重新启动VFP时每一次都得到一条错误信息无疑是一件痛苦的事情,因此在那种情况下,这一选项将是clear events并返回到主程序,因此应用程序能够以有序的方式关闭并返回命令窗口。如果这是一个VFP的运行时版本,我们将只清理并退出。在上述两种情况中,在所有的数据工作期的所有游标上,我们都有将使用自定义的RevertTables方法来执行一个tablerevert(.T.)。因此,以这种方法退出,我们不会得到一个附加的错误(比如那个臭名远扬的“uncommitted changes”错误)。
处理特殊的错误
到目前为止,我们已经讨论了关于错误处理方案的一般性的问题:每一个错误都会造致“取消,继续,重试,退出”对话框的出现。这对于可预知的错误是不适当的,它仅适用于不可预知性的错误。可预知的错误将在可能造致错误的对象的Error方法中处理。
作为处理特殊错误的一个例子,我们将考虑看一看SFMaintForm类(在SFFORMS.VCX中),这是一个特别为数据输入表单设计的SFForm类的一个子类。这是一个关于一个对象如何处理环境和可预知错误的好例子。
由于SFForm的Error方法以简单的传递错误到SFErrorMgr的ErrorHandler方式调用HandleError方法,我们将不考虑用SFMaintForm中的HandleError来处理特殊的基于数据的错误。以下是HandleError的代码:

local lnError, ;
lcMethod, ;
lcReturn, ;
loObject
with This

* Get the error number and method。

lnError = .aErrorInfo[.nLastError, cnAERR_NUMBER]
lcMethod = upper(.aErrorInfo[.nLastError, ;
cnAERR_METHOD])
do case

* Handle "DataEnvironment already unloaded" by not
* displaying anything。

case lnError = cnERR_DE_UNLOADED
lcReturn = ccMSG_CONTINUE

* Handle a problem in the DataEnvironment。

case 'DATAENVIRONMENT' $ lcMethod
lcReturn = .ErrDataEnvironment(lnError)

* Handle a trigger failed。

case lnError = cnERR_TRIGGER_FAILED
lcReturn = .ErrTriggerFailed()

* Handle a field rule failed by calling
* ErrFieldRuleFailed()。If it returns an object
* reference, we'll set focus to that object so the
* user can correct the problem。

case lnError = cnERR_FIELD_RULE_FAILED
loObject = .ErrFieldRuleFailed()
if not isnull(loObject)
.ActivateObjectPage(loObject)
loObject.SetFocus()
endif not isnull(loObject)
lcReturn = ccMSG_CONTINUE

* Handle a table rule failed。

case lnError = cnERR_TABLE_RULE_FAILED
lcReturn = .ErrTableRuleFailed()

* Handle a primary/candidate index violation。

case lnError = cnERR_DUPLKEY
lcReturn = .ErrDuplicatekey()

* Handle the case where someone else has the record
* locked。

case lnError = cnERR_RECINUSE
lcReturn = .ErrRecordInUse()

* Handle the case where the record was modified by
* another user during a delete。

case lnError = cnERR_RECMODIFIED and ;
lcMethod = 'DeleteRecord'
messagebox(ccERR_REC_MODIFIED, MB_ICONSTOP, ;
_screen.Caption)
.Refresh()
lcReturn = ccMSG_CONTINUE

* Handle the case where the record was modified by
* another user during an edit。

case lnError = cnERR_RECMODIFIED
lcReturn = .ErrRecChangedByAnother()

* Otherwise use the default error handler。

otherwise
lcReturn = dodefault()
endcase
endwith
return lcReturn
你可能会认为,一个处理特殊错误的子程序将可能由一组case语句组成,以处理所有可预知的错误和一个otherwise语句传递所有不可处理的错误到SFErrorMgr。在SFMaintForm的情况中,我们将处理以下错误:
· DataEnvironment错误或触发器错误或字段规则失败:在这里,我们将注意这些错误的更多细节。
· 表规则失败,基本/候选索引违反,或其它人锁住了记录:其它的在系统设计时应考虑的问题(例如使用系统指定的关键字和使用乐观或悲观锁),关于这些,我们没有更多的选择;只能提交给用户,由用户处理这些问题。因此我们只调用特定的方法显示适当的信息给用户即可。
· 当我们试着删除一个记录时,它已被其它用户修改了:我们将显示一条信息给用户并重新显示表单内容为修改后的内容。
· 当我们想修改一条记录时,它已被其它用户修改了:我们将使用解决冲突的代码来解决这种问题(在这里,我们不再列出这些代码;你可以自己查看ErrRecChangedByAnother方法中的代码)。
当然,这不是全部的可捕捉的错误,但是一个好的典型例子。
数据环境
尽管DataEnvironment有自己的Error方法,但类没有DE,因此我们必须人为的放置代码到我们所建立的每一个数据表单的DataEnvironment.Error中。有趣的是,如果DE的Error方法中没有代码,它将自动调用其表单的Error方法。这样我们将在SFMaintForm的ErrDataEnvironment方法中处理DE的错误。这些处理包括了“表或数据库未找到”,“表访问拒绝”,“表在使用中”,和“主关键字无效”等错误。在一般情况下,除了显示错误信息和关闭表单外我们没有别的选择。然而,我们还希望得到一些错误处理服务,因此我们在设置了lDisplayErrors属性为.F.后,调用oError.ErrorHandler。这会使错误被登录但不显示错误信息。我们将会在运行“关闭表单”前显示我们自己的错误信息。
要注意的一点是“DataEnvironment already unloaded”错误。这是由于早期的错误而使表单已关闭,因此当我们接收到该信息时什么也不必做。
触发
如果触发器失败,致使触发器失败的对象的Error方法被激活,而不是被触发器(RIError procedure)设置的on error子程序。这是一个大问题:RIError设置公共变量pnError为一个非零的值,这是一个被其它例程使用的,以便知道触发器已经失败的变量。设想以下情形:用户在一个表单中做了某些事情,比如删除一个记录使触发器失败而致使错误处理程序被调用,但由于触发器是在一个对象的Error方法中被引发,该方法被调用要比调用保存在数据库中的RIError例程要好。当Error方法被执行时,执行返回到触发器。但是,由于pnError的值仍然是零(没有改变),触发器代码不知道发生了错误,因此它继续执行。最终结果是,触发器可能部份失败。
这里是一个具体的例子。当ORDERS对于ORDITEMS具有一个限制删除规则时,CUSTOMER对于ORDERS具有一个级联删除规则。当前的CUSTOMER记录有两条相关的ORDERS记录,第一条没有关联的ORDITEMS记录,第二条有一条相关的ORDITEMS记录。当你删除CUSTOMER记录时,你想会发生什么情况?你会得到一条触发失败的错误信息,但你会发现CUSTOMER记录的第一条ORDERS记录已被删除。仅第二条ORDERS记录还存在,当然,现在它成为一个没有父记录的记录。
(你可能会认为这是一个不可思议的例子,我在实际中确实发生了这样的问题。)
解决的办法是,必须在错误处理程序中将pnError设置为非零的值;如果你查看SFMaintForm的ErrTriggerFailed方法代码,你会发现它只做了这一点。然而,那样做并没有完全解决这一问题:VFP引用完整性(RI)生成器生成的RIDelete和RIUpdate过程中的一个BUG(删除和更新一个子记录)也必须改正(参见下面的关于RIDelete的代码)。如果pnError非零,这些例程设置llRetVal的值(返回值)为.F.,以告诉其它例程触发失败。然而,由于RIOpen例程被调用来在非缓冲方式下打开子表以检查与父表相关的记录,下一级的触发器(如,检查子表的子表)在未遇到unlock语句时不会被激活,这出现在llRetVal设置之后。这样,当试着删除或更新孙记录(致使触发错误信息出现的记录)时发生的错误将不会造成llRetVal被设置为.F.,因此该等级的触发器不会失败(即使它确实失败了)。解决这一问题的办法是象下面那样,移动llRetVal语句到unlock命令以后。由于RIDelete和RIUpdate是普通的例程,你可以从数据库的储存代码中复制这些代码,粘贴它们到数据库的储存代码的结束部份(在RI脚注行后)并在这里修改。采用这种方法,你不必在每次重新生成RI代码后,都进行相同的修改。

procedure RIDELETE
local llRetVal
llRetVal=.t.
IF (ISRLOCKED() and !deleted()) OR !RLOCK()
llRetVal=.F.
ELSE
IF !deleted()
DELETE
IF CURSORGETPROP('BUFFERING') > 1
=TABLEUPDATE()
ENDIF

*** 剪切下面一行...
* llRetVal=pnerror=0
ENDIF not already deleted
ENDIF
UNLOCK RECORD (RECNO())
*** ... 把它粘贴到这里

llRetVal=pnerror=0
RETURN llRetVal
违反字段规则
在VFP 5(包括5.0a)中的函数sys(2018)和aerror()中存在一个BUG,当一个字段规则违反时,实际的字段名将不可用。相反,你得到两个字串之一:要么是字段的RuleText属性(如果它是填写了的),要么是一个一般的信息“字段验证规则冲突”。
怎么会是这样?那么,怎样才能象你希望的那样:在该字段没有RuleText时,显示一条信息“请为字段<字段标牌>输入一个合法的值”?问题是如果你不知道那一个字段的规则被违反,你又如何知道用那一个字段的标牌呢?假如你想设置焦点到发生错误的字段然后显示错误信息,更加方便用户修改出问题的字段的值?假如你想改变那个控件的背境颜色,怎样显而易见的告诉用户哪一字段发生了错误呢?
在VFP 5.0(不是5.0a)中,要改正这一问题,我们需要做两件事中的一件:如果这个串是“字段验证规则冲突”,我们将从串中挖出该字段。如果那不是一个串,我们必须在数据库中找到给出RuleText属性的字段。幸运的是,在VFP 5.0a,一个未编档函数aerror(),它可以建立一个5个元素的数组,其中包含了字段号,利用它我们可以找到我们要找的字段名。
SFMaintForm.ErrFieldRuleFailed方法处理这种behavior;代码如下:
local loObject, ;
lcAlias, ;
lcTable, ;
lcMessage, ;
lcField, ;
lnSpace1, ;
lnSpace2, ;
lnI, ;
lcText

* If the field rule was checked and failed because
* the user clicked on a button with the Cancel
* property set to .T. or if the button has an lCancel
* property (which is part of the SFCommandButton base
* class) and it's .T., don't bother doing anything
* else。

loObject = sys(1270)
if lastkey() = 27 or (type('loObject') = 'O' and ;
type('loObject.lCancel') = 'L' and loObject.lCancel)
return .NULL.
endif lastkey() = 27 ...
with This

* Figure out which field failed。

lcAlias = alias(.aErrorInfo[.nLastError, ;
cnAERR_WORKAREA])
lcTable = cursorgetprop('SourceName', lcAlias)
lcMessage = .aErrorInfo[.nLastError, cnAERR_MESSAGE]
lcField = lower(lcAlias + '.' + ;
field(.aErrorInfo[.nLastError, cnAERR_TRIGGER], lcAlias))

* Display the error message and find the object whose
* ControlSource is the field。

messagebox(lcMessage, MB_ICONSTOP, _screen.Caption)
loObject = .FindControlSourceObject(lcField)

* Set the lFieldRuleFailed flag to .T.

.lFieldRuleFailed = .T.
endwith
return loObject
ErrFieldRuleFailed要做的第一件事是检查用户是否单击了表单中的“取消”按钮,并试着设置焦点到该按钮,以激活当前控件的字段的验证规则。由于在此情况下,它不必再显示错误信息,因此ErrFieldRuleFailed除此之外不做任何事情,简单返回。
如果用户没有选择”取消”,ErrFieldRuleFailed指出那一字段发生了问题,它调用FindControlSourceObject方法来找出表单中的那一对象是绑定到该字段。如果找到该对象,它返回一个引用到对象,因此HandleError可以设置焦点到它。
ErrFieldRuleFailed也设置一个叫lFieldRuleFailed的表单属性为.T.,它可用于当字段规则失败时,控件的Valid方法来避免控件失去焦点。需要它的原因是因为字段验证规则违反程序在控件的Valid之前被激活。当错误被处理后,Valid方法激活,正常地返回.T.,允许控件失去焦点(即使它包含了一个错误的值)。以下是避免这一情况的SFTextBox的Valid方法代码:
if type('Thisform.lFieldRuleFailed') = 'L' and ;
Thisform.lFieldRuleFailed
Thisform.lFieldRuleFailed = .F.
return 0
endif type('Thisform.lFieldRuleFailed') = 'L' ...
例子
要查看examples是如何在这个错误处理机制中处理预料不到的错误和处理的细节,运行示例文件中的MYAPP.APP。从文件菜单中选择“错误表单一”并单击“这将造成一个错误”按钮。该按钮的Click方法包含了两个错误,因此你在第一个错误出现时的对话框中选择继续时,第二个错误将出现并且错误对话框将再次出现。如果这次你选择取消,第二个错误不会出现,但程序停留在运行状态且表单仍然是打开的。选择退出来干净地退出程序,恢复菜单条,关闭表单,并重新设置环境到运行前的状态。重试,当然,造成相同的错误信息出现,由于该问题已经改正。
要查看特殊的错误是如何处理的,从文件菜单中选择Customer表单。从文件菜单中选择新建,在Customer ID中输入“ALFKI”,然后从文件菜单中选择选择保存。你会得一条错误信息“Customer ID 已经存在”;由于该字段是该表的主关键字并且已经的一个名为ALFKI的记录存在于该字段中,一条主关键字违反的错误将发生并将象展示的那样被处理。要查看字段规则失败的结果,在Company字段中输入“Test”;该字段有一个规则避免输入该值。当你想离开该字段时,你会看到字段规则失败的错误信息。清空Company字段。移运光标到City字段并输入“Regina”,然后从文件菜单中选择保存。你会得到一条表规则失败的错误信息;该表有一个规则拒绝接受City字段的值为“Regina”(最后,谁想生活在这里)。从文件菜单中选择撤消来移去新增加的记录。
要查看当”取消”按钮被按下时,SFMaintForm是如何避免一个字段规则失败信息,在Company字段上输入”test”并在离开该字段前单击“撤消”按钮。
要查看DE问题是怎样处理的,在文件管理器中将CUSTOMER.DBF改名为CUST.DBF,运行MYAPP并打开Customer表单。注意在你回应所出现的错误信息后,表单关闭了。将CUST.DBF的名字改回到CUSTOMER.DBF。
要查看触发器失败问题,退出应用程序,打开TESTDATA数据库,浏览CUST_ORDERS视图。该视图显示表CUSTOMER,ORDERS,和ORDITEMS中的记录信息。注意ALFKI客户有一些订货但第一个(ORDER_ID=10062)没有订货细节(LINE_NO是.NULL.)。运行MYAPP,打开Customer表单,从文件菜单中选择删除,当确认窗口出现时选择确认确认删除。注意触发失败信息将出现;这是由于在表ORDERS与ORDITEMS间存在限制删除的规则。然而,注意多次得到这一信息,并在最后一次反应后,ALFKI客户被删除了。退出程序,打开CUSTOMERS,注意ALFKI真的已经被删除了。现在
USE ORDERS ORDER CUST_ID
并注意很多客户ALFKI的订单记录仍然存在;当然,它们都成了没有父记录的孤记录。
要修复这一问题,按以下步骤:
· CLOSE ALL
· 解压DATA.ZIP(我们弄乱了数据,因此我们需要恢复它们)。
· OPEN DATABASE TESTDATA
· MODIFY PROCEDURES
· 象前面描述的那样,将RIDelete过程中的llRetVal移到UNLOCK语句后。保存并关闭代码窗口。
· MODIFY PROJECT MYAPP
· 打开类库SFForms中的SFMaintForm类,进入ErrTriggerFailed方法的代码窗口,到代码尾部,去掉pnError的注释。存盘并关闭代码窗口。
· 重建MYAPP并运行它。
· 打开Customer表单,从文件菜单中选择删除,当被询问确认删除时选择确认确认删除。
此时,你会得到一条单一的触发器失败信息,并且ALFKI客户任然存在。错误被控制了!
要检查的其它东西:
· 要查看SFErrorMgr是如何登录错误的,打开ERRORLOG表并浏览它。特别注意MEMVARS字段;它包含了发生错误时在调用堆栈上的每一个过程中的每一个变量的值。
· 修改APPLIC.INI文件中的Developer为Yes。该值被过程STARTUP.PRG用于设置oError的lDeveloper属性。打开资源管理器并按下F5(这是必要的,这样VFP才知道INI文件被修改了),然后运行MYAPP。从文件菜单选择“错误表单1”并单击文件头按钮。注意一个调试选项出现在错误对话框中。这对于调试一个运行中的程序来说是真正有用的。当然,跟踪窗口显示SFErrorMgr类的是ErrorHandler方法代码,由于那是SET STEP ON命令执行的地方。要返回到真正造成错误的代码中,你必须为错误处理链中的每一例程单击Debugger的工具条中的跳出按钮一次。
· 在“错误表单1”中的“这将造致一个错误但不会发生什么”按钮正象它所说的那样做:单击它不会发生什么。如果你查看它的Click方法,看起来会令人惊呀,因为在其中有两个错误。然而,查看Error方法并注意与其它对象不同的地方,它直接调用其父容器的Error方法。这是一个问题:该按钮是放置在一个PageFrame的页面中,并且该页是一个VFP的基类页,它没有Error方法代码,这意味着当一个错误出现时什么也不会发生。就象ON ERROR*一样,这是一个使你的程序不出现错误住处的好方法。不是没有错误,只是不显示错误信息。这就是为什么所有SFCTRLS.VCX中的基类要向上级容器类查找直到找到第一个具有Error方法代码的类。
多样的错误信息
这里是一些关于VFP错误处理时,你应知道的东西。
· 在VFP 6以前的版本中,VFP自动服务器不能适当地返回错误条件到它的调用程序。它们被附加的comreturnerror()函数修改了。该函数移动COM例外结构用错误发生信息。它接受两个参数:cExceptionSource,服务器的名字,和cExceptionText,你想返回的信息。MYAPP.APP中的form1调用名为MyServer的自动服务程序Test()方法演示了这一点(MyServer项目包含在sample文件中)。
· 如果type()在错误处理链中的任何地方被使用,你不会总是得到正确的错误信息。理由是type()使用了某些VFP的错误处理内核,作为结果,当被type()息检测的变量或属性不存在时,message()的内容可能被复写。这就是为什么有时你的错误处理程序会报告“变量未找到”信息。在VFP 98中,尽可能地使用vartype();它比type()更快且避免了以上问题。
· 在VFP 5中,当绑定控件的数据源字段的验证规则违反时该控件的Error方法被调用。这允许你控制显示什么样的错误信息和怎样显示它们。在VFP 3中,这些错误是不可捕捉;VFP适当地为字段显示Rule文本(若Rule文本为空,则显示一个难看的文字信息)在一个你不能控制的messagebox()对话框中。
· 尽管作为local定义的变量在定义它的过程以外是不可见的,但list memory命令可以看见调用堆栈中全部例程中定义的全部变量。这是一件好事情,因为它允许你在发生错误时,取得全部变量的一个信息并将其登录到一个文件。SFErrorMgr的LogError方法展示了怎样做。
· list status命令对错误处理的帮助并不是很大:它只能看见当前数据会话区中的东西(例如打开的表和SET命令的设置),因此,如果错误处理程序是在默认数据工作期,它不能发现私有数据工作期中的表单发生的错误。如果这对你很重要,你可以在使用该命令前转换到表单的数据工作期或你可以派生SFErrorMgr的一个子类进入表单的oError属性,这样作为表单就都在相同的数据工作期了。
· 出于性能方面的原因,当错误发生时,你可能不想一次一步地调用父类的代码,因此你可以直接从一个对象转跳到表单级的Error方法或SFErrorMgr.ErrorHandler。我的意见是,发生错误时,性能不是考虑因素,因此我在本文中保持了类设计清晰和条理。
· error命令是有趣的:它致使用一个错误被触发。如果你想象VFP一样处理某些类型的“软”错误时,使用它是很方便的。例如,如果file()返回了.F.指出文件不存在,你可以使用error命令强制错误处理器激活这样你可以得到一般错误服务(登录,显示,等)。我趋向于尽量少用这种方法;毕竟,当你正在处理一个硬错误时,还会受到软错误检查的干扰。同时,这还取决于你在那里使用error命令,你可能不会获得正确的方法名称和行号信息,因为它们将反映error命令是在那里被使用而不是你在那里发现的问题。
· 当对象方法调用的程序发生错误时,对象的Error方法先于on error激活。这意味着可以在编程中用两种不同的机制来处理一个错误,这要看程序是被如何调用的。这样,在编码中的错误处理不象在对象中那么简单。这是把它们从PRG库中移到对象库中的另一个理由。
· 以上观点还有另一个有趣的方面:如果你的read events语句在一个全局对象的方法中(例如一个application对象),该对象的Error方法的效果就相当于一个全局错误处理器,因为该对象总是在调用堆栈上。你可以用on error结束这种情况,这样它将只处理不从对象中调用的,PRG代码中的错误。
· 你不能正常地捕捉以下情况:当用户在TextBox中输入一个无效的日期值时;VFP显示“Invalid Date”并不会激活该控件的Valid方法。你可以用set notify off命令关闭“Invalid Date”信息,但是你如果想激活控件的Valid方法(例如,要给出一条不同的信息或带出日历控件),你必须设置控件的StrictDateEntry属性为0。这里要注意的是:如果用户输入了不可用的日期,它将会被置为空格,因此如果你想重新显示填入的值,你必须在控件的GotFocus方法中保存它并在Valid中恢复它。
· on error不捕捉报表中的和菜单命令的skip for子句中的错误;这些错误是不可捕捉的,因此确信你的报表和菜单中的skip for子句中的条件表达式是经过彻底测试的。要确认这一点,编辑APPLIC.INI并设置NoSuchVariable入口为No。打开Windows Explorer并按下F5(这是必要的,这样VFP会发现INI文件已经被修改),然后运行MYAPP。在菜单上单击,并注意VFP错误的出现。
结论
在本文中,探讨了两种最佳错误处理方案(局部处理大多数错误及剩下的由全局处理)。此方案已成功地运用于一系列的应用程序中,尽管我们仍在不断地完善它。我希望你能找到对你有用的地方。如果你对它有任何改进或有任何改进建议,请通知我。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
使用AutoIT测试系统登录实例七(COM错误截获)
第八章 表单设计
第四章 Controller接口控制器详解(7 完)——跟着开涛学SpringMVC
jquery.form.js中文API jquery ajax 提交表单插件
FileItem类
让 SpringMVC 接收多个对象的4种方法 – 码农网
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服