在 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 開發、項目管理經驗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章