(这是一篇我几年前发表在IBM developerWorks上的文章,在此转帖,构成面向对象的LotusScript的系列文章之一篇,并为后续的讨论做准备。原来developerWorks上文章的格式要求在此页显示不太美观,稍作调整。有些英文粘贴后会被去掉中间的空格,没有检查到的就还会存在,请注意分词:)代码着色比原本的一色黑白好看很多。)
事件是面向对象语言普遍支持和使用的一种模式。事件不仅在与用户交互的系统中应用很广泛,设计对象时恰当地采用事件对写出结构清晰、独立的代码也很有帮助。LotusScript支持事件,各个UI对象公布的事件在程序中都经常使用。不过在LotusScript支持的三种对象:Notes对象、自定义对象和OLE对象中,只有Notes对象支持事件。也就是说我们只能使用Notes类公布的事件,无法在自定义类中定义事件。
那么,是否可以在LotusScript模拟事件?
事件处理的核心就是当某个“状态”变化时一个程序(事件源event emitters)通知预订(subscribe)处理此事件的另一个程序(事件消费者event consumers),很多语言通过回调函数实现这样的机制。LotusScript不支持任何形式的“函数指针”,所以只能另想办法。
下面分析一个实际问题。
设想有一个资产管理的数据库,其中要处理诸如接收、转移、注销的多个流程。我们将处理流程的公共代码都放在一个流程类SimpleFlow中。具体的工作流只要创建一个SimpleFlow的实例然后调用它的Submit方法。每个流程会有一些特定的业务逻辑,比如在资产注销流程提交前,要检查一个Amount域的值,如果金额大于5000需要弹出一个对话框让用户输入更多信息;流程提交后再更新对应的资产文档。这些业务逻辑可以放在很多地方,比如在表单上提交动作的按钮或操作中,在调用SimpleFlow的Submit方法前后加入。如果采用事件的方式考虑,我们希望在Submit之前和之后分别引发事件QuerySubmit和PostSubmit,在它们的处理程序(eventhandlers)中添加流程实例特定的代码。
为了使讨论集中,本文的示例代码都只包含与主题相关的部分,不包含处理流程的细节以及错误处理代码。
Public Class SimpleFlow '定义公共对象变量 '流程变量 Private strFlow As String Private strNode As String Private strAction As String 'Notes对象 Private s As NotesSession Private ws As NotesUIWorkspace Private uidoc As NotesUIDocument Private doc As NotesDocument Private db As NotesDatabase '下面是SimpleFlow公开的一些属性 Public Property Get FlowName As String FlowName=strFlow End Property Public Property Get NodeName As String NodeName=strNode End Property Public Property Get ActionName As String ActionName=strAction End Property Public Property Get MainUIDoc As NotesUIDocument Set MainUIDoc=uidoc End Property Public Property Get MainDoc As NotesDocument Set MainDoc=doc End Property Public Property Get MainDb As NotesDatabase Set MainDb=db End Property Public Sub New(flowname As String,nodename As String,actionname As String) strFlow=flowname strNode=nodename strAction=actionname '初始化Notes对象 Set s=New NotesSession Set db=s.CurrentDatabase Set ws=New NotesUIWorkspace Set uidoc=ws.CurrentDocument Set doc=uidoc.Document End Sub '供外部调用的主方法 Public Sub Submit '如果QuerySubmit返回False,则不再继续 If Not QuerySubmit(Me) Then Exit Sub End If '处理流程的代码 Call PostSubmit(Me) End Sub End Class
SimpleFlowLib中还定义了 SimpleFlow类调用的QuerySubmit和PostSubmit方法。
Private Function QuerySubmit(flowObj As SimpleFlow) As Boolean '流程提交之前运行 '返回Boolean值,如果为True则继续Submit;否则取消 QuerySubmit=True Dim ws As New NotesUIWorkspace '检查流程的名称和状态 If flowObj.FlowName="Asset" And _ flowObj.NodeName="Draft" And _ flowObj.ActionName="Submit" Then 'DlgInfo是一个用来输入更多信息的表单 If Not ws.DialogBox("DlgInfo") Then '如果用户取消,就不再提交 QuerySubmit=False End If End If End Function
Private Sub PostSubmit(flowObj As SimpleFlow) '流程提交之后运行 Dim viewAsset As NotesView '从SimpleFlow的属性获得对当前数据库的引用 'Assets视图包含资产文档并且第一列按AssetID排序 Set viewAsset=flowObj.MainDb.GetView("Assets") '资产文档 Dim docAsset As NotesDocument '检查流程的名称和状态 If flowObj.FlowName="Asset" And _ flowObj.NodeName="5.Manager Approval" And _ flowObj.ActionName="Approve" Then Set docAsset=viewAsset.GetDocumentByKey(flowObj.MainDoc.AssetID(0),True) If Not docAsset Is Nothing Then '更新资产文档 docAsset.ApprovalStatus="Approved by manager" Call docAsset.Save(True,False) End If End If End Sub
这样在资产注销的主表单中,我们只要引用SimpleFlowLib,然后在某个按钮或操作中建立一个SimpleFlow对象并提交就可以了。
这样的模拟可以说和事件的本质相差很多。作为的事件源的SimpleFlow对象调用固定的方法而不是由消费者添加事件处理程序。结果就是QuerySubmit和PostSubmit方法只能和SimpleFlow类写在同一个ScriptLibrary中。SimpleFlowLib失去了部分通用性,每个使用此ScriptLibrary的数据库都可能需要修改QuerySubmit和PostSubmit方法。
为了克服上面的缺点,我们采用另一种更接近事件本质的方式来实现模拟。下面是一个简单的公布一个Demo事件的类。
Public Class EventObject '用于给Demo事件添加event handler Public DemoEvent As String '在此方法中触发Demo事件 Public Function Run Call OnDemoEvent End Function '运行event consumer添加的event handler Private Function OnDemoEvent Execute DemoEvent End Function End Class
在另一个方法中创建一个EventObject对象并且添加Demo事件的handler。
Public Function Main Dim eo As New EventObject Set demoObj = New Demo Dim demoHandler As String demoHandler={MessageBox("Hello Everybody!")} eo.DemoEvent=demoHandler Call eo.Run() End Function
这里的demoHandler可以和普通的方法一样引用Notes对象或者自定义的对象和方法。通常,事件消费者和EventObject不会定义在同一个模块中,这个时候,被调用的对象和方法必须是public的,并且还需要在定义demoHandler时加上Use语句。这样LotusScript在Execute语句中装入(load)临时模块时才能访问到demoHandler中引用的对象和方法。
下面在TestLib中定义一个用于测试的类。
'用于测试的一个简单的类 Public Class Demo Public Function DoSomething(lan As String) Msgbox "do something in " & lan End Function End Class '一个Demo类的实例 Public demoObj As Demo
然后在添加Demo事件的handler时就可以引用它们。
'引用自定义的Demo对象的方法 demoHandler={use "Test":demoObj.DoSomething("LotusScript")}
注意在上面的代码中使用了冒号(:),这是LotusScript从Basic中继承下来的用于在一行中连接多个语句的方法。这个特性很少用到(在Designer中如果用冒号隔开多条语句,Designer会自动将它们分成多行并去掉冒号),不过在这里却恰到好处地让demoHandler的定义简洁可读,不用写成多行字符串。
现在我们用上面的方法重新实现SimpleFlow的QuerySubmit和PostSubmit事件。
'在(Declarations)中添加一个公共变量,用于保存事件处理程序返回的结果。 Public EventResult As Boolean '在SimpleFlow中添加下列模拟事件的代码。 '定义QuerySubmit和PostSubmit事件 Public QuerySubmit As String Public PostSubmit As String '运行QuerySubmit的event handler Private Function OnQuerySubmit OnQuerySubmit=Execute(QuerySubmit) End Function '运行PostSubmit的event handler Private Function OnPostSubmit OnPostSubmit=Execute (PostSubmit) End Function '修改Submit方法 Public Sub Submit '如果QuerySubmit返回False,则不再继续 Call OnQuerySubmit If Not EventResult Then Exit Sub End If '处理流程的代码 Msgbox ("oops") Call OnPostSubmit End Sub
同时,我们可以把原来包含了业务逻辑的代码移出SimpleFlowLib,放入TestLib中,并将其访问修饰符(access modifier)改为Public。这样SimpleFlowLib就可以作为一个标准的ScriptLibrary被各个流程公共使用。
'在这个新增加的方法中,添加SimpleFlow对象的事件处理程序,然后提交。 Public Function SubmitFlow Set flowObj = New SimpleFlow("Asset", "Draft", "Submit") flowObj.QuerySubmit={Use"TestLib":EventResult=QuerySubmit} flowObj.PostSubmit={Use"TestLib":PostSubmit} Call flowObj.Submit() End Function
真正的事件机制往往比上面的示例复杂很多,比如事件源可以向处理程序传递参数,事件消费者可以为一个事件添加多个处理程序,也可以去除已有的处理程序。下面的代码部分地模拟了这些特性,创建了一个可以被继承的通用的实现了事件机制的类EventPublisher。在SimpleFlow类继承这个类后,就可以自由地定义事件,引用该类的实例的程序也可以动态的增加删除事件处理程序。
Public EventResult As Boolean Public Class EventPublisher '定义通用的事件列表 Private EventList List As Variant '添加事件处理程序 Public Function AddEventHandler(eventName As String, handler As String) Dim handlerList List As String Dim v As Variant If Iselement(EventList(eventName)) Then v=EventList(eventName) v(handler)=handler EventList(eventName)=v Else handlerList(handler)=handler EventList(eventName)=handlerList End If End Function '去除事件处理程序 Public Function RemoveEventHandler(eventName As String, handler As String) '需要在(Options)中添加%INCLUDE "lserr.lss" On Error ErrListItemDoesNotExist Goto ExitFunction Dim handlerList As Variant handlerList=EventList(eventName) Erase handlerList(handler) EventList(eventName)=handlerList ExitFunction: Exit Function End Function '运行EventList中某事件的所有处理程序 Private Sub OnEvent(eventName As String) If Iselement(EventList(eventName)) Then Dim v As Variant v=EventList(eventName) Forall handler In v Execute handler End Forall End If End Sub '重新改写的Submit方法 Public Sub Submit '如果QuerySubmit返回False,则不再继续 Call OnEvent("QuerySubmit") If Not EventResult Then Exit Sub End If '处理流程的代码 Msgbox ("oops") Call OnEvent("PostSubmit") End Sub End Class
在TestLib中引用SimpleFlow的代码也做相应的修改:
'采用通用事件 Public Function SubmitFlowEx Set flowObj = New SimpleFlow("Asset", "Draft", "Submit") Call flowObj.AddEventHandler("QuerySubmit",{Use"TestLib":EventResult=QuerySubmit}) '可以增加任意多个event handler 'Call flowObj.AddEventHandler("QuerySubmit",{Use"TestLib":QuerySubmitHandler2}) Call flowObj.AddEventHandler("PostSubmit",{Use"TestLib":PostSubmit}) '去除event handler Call flowObj.RemoveEventHandler("QuerySubmit",{Use"TestLib":QuerySubmitHandler2}) Call flowObj.Submit() End Function
通过模拟事件可以写出更方便移植的自定义类。不过与很多语言本身的事件机制相比,还是有很多局限性。事件处理程序仅仅通过一个字符串来传递,无法检查类型和签名,缺乏安全性。只有通过公共变量才能在事件源和消费者之间传递事件的相关信息。事件处理程序必须定义在一个ScriptLibrary中,事件源才能通过Use语句引用并访问。这些限制都使得模拟事件的应用不甚方便。