zoukankan      html  css  js  c++  java
  • 在 LotusScript 中为自定义对象模拟事件

    前言

    事件是面向对象语言普遍支持和使用的一种模式。事件不仅在与用户交互的系统中应用很广泛,设计对象时恰当地采用事件对写出结构清晰、独立的代码也很有帮助。LotusScript 支持事件,各个 UI 对象公布的事件在程序中都经常使用。不过在 LotusScript 支持的三种对象:Notes 对象、自定义对象和 OLE 对象中,只有 Notes 对象支持事件。也就是说我们只能使用 Notes 类公布的事件,无法在自定义类中定义事件。

    那么,是否可以在 LotusScript 模拟事件?

    事件处理的核心就是当某个“状态”变化时一个程序(事件源 event emitters)通知预订(subscribe)处理此事件的另一个程序(事件消费者 event consumers),很多语言通过回调函数实现这样的机制。LotusScript 不支持任何形式的“函数指针”,所以只能另想办法。

    下面分析一个实际问题。


    实现一

    设想有一个资产管理的数据库,其中要处理诸如接收、转移、注销的多个流程。我们将处理流程的公共代码都放在一个流程类 SimpleFlow 中。具体的工作流只要创建一个 SimpleFlow 的实例然后调用它的 Submit 方法。每个流程会有一些特定的业务逻辑,比如在资产注销流程提交前,要检查一个 Amount 域的值,如果金额大于 5000 需要弹出一个对话框让用户输入更多信息;流程提交后再更新对应的资产文档。这些业务逻辑可以放在很多地方,比如在表单上提交动作的按钮或操作中,在调用 SimpleFlow 的 Submit 方法前后加入。如果采用事件的方式考虑,我们希望在 Submit 之前和之后分别引发事件 QuerySubmit 和 PostSubmit,在它们的处理程序(event handlers)中添加流程实例特定的代码。

    为了使讨论集中,本文的示例代码都只包含与主题相关的部分,不包含处理流程的细节以及错误处理代码。


    清单 1. SimpleFlow 类(代码包含在一个名为 SimpleFlowLib 的 Script Library 中)
    				
    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 方法。


    清单 2. QuerySubmit 代码
    				
    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
    


    清单 3. PostSubmit 代码
    				
    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 失去了部分通用性,每个使用此 Script Library 的数据库都可能需要修改 QuerySubmit 和 PostSubmit 方法。


    另一种思路

    为了克服上面的缺点,我们采用另一种更接近事件本质的方式来实现模拟。下面是一个简单的公布一个 Demo 事件的类。


    清单 4. EventObject 类(代码包含在一个名为 EventObjectLib 的 Script Library 中)
    				
    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。


    清单 5. 添加事件处理程序(代码包含在一个名为 TestLib 的 Script Library 中)
    				
    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 中定义一个用于测试的类。


    清单 6. TestLib 中的 (Declarations) 部分
    				
    '用于测试的一个简单的类
    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 时就可以引用它们。


    清单 7. 稍微复杂的事件处理程序
    				
    '引用自定义的 Demo 对象的方法
    demoHandler={use "Test":demoObj.DoSomething("LotusScript")} 
    

    注意在上面的代码中使用了冒号 (:),这是 LotusScript 从 Basic 中继承下来的用于在一行中连接多个语句的方法。这个特性很少用到(在 Designer 中如果用冒号隔开多条语句,Designer 会自动将它们分成多行并去掉冒号),不过在这里却恰到好处地让 demoHandler 的定义简洁可读,不用写成多行字符串。


    重新实现 SimpleFlow 的事件

    现在我们用上面的方法重新实现 SimpleFlow 的 QuerySubmit 和 PostSubmit 事件。


    清单 8. 修改后的 SimpleFlowLib
    				
    '在 (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 就可以作为一个标准的 Script Library 被各个流程公共使用。


    清单 9. 修改后的 TestLib
    				
    '在这个新增加的方法中,添加 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 
    


    再进一步

    真正的事件机制往往比上面的示例复杂很多,比如事件源可以向处理程序传递参数,事件消费者可以为一个事件添加多个处理程序,也可以去除已有的处理程序。下面的代码部分地模拟了这些特性。在 SimpleFlow 类中添加了通用的事件处理代码后,就可以自由地定义事件,引用该类的实例的程序也可以动态的增加删除事件处理程序。


    清单 10. SimpleFlow 中通用的事件处理代码
    				
    '定义通用的事件列表
    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 
    

    在 TestLib 中引用 SimpleFlow 的代码也做相应的修改:


    清单 11. 再次修改后的 TestLib
    				
    '采用通用事件
    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 
    


    结束语

    通过模拟事件可以写出更方便移植的自定义类。不过与很多语言本身的事件机制相比,还是有很多局限性。事件处理程序仅仅通过一个字符串来传递,无法检查类型和签名,缺乏安全性。只有通过公共变量才能在事件源和消费者之间传递事件的相关信息。事件处理程序必须定义在一个 Script Library 中,事件源才能通过 Use 语句引用并访问。这些限制都使得模拟事件的应用不甚方便。


    参考资料

    学习

    获得产品和技术

    讨论

    关于作者

    Starrow Pan,自由软件开发人员,具有 7 年 IT 行业经历和丰富的 Notes 开发、项目管理经验。

  • 相关阅读:
    hadoop_并行写操作思路_2
    hadoop_并行写操作思路
    Hadoop_Block的几种状态_DataNode
    KMP算法_读书笔记
    德才论
    换个格式输出整数
    继续(3n+1)猜想
    害死人不偿命的(3n+1)猜想
    c# number求和的三种方式
    c# 中的协变和逆变
  • 原文地址:https://www.cnblogs.com/hannover/p/1924024.html
Copyright © 2011-2022 走看看