As a followup to last night's post, after receiving some helpful advice from both Peter Presnell and the Batman to my Robin, I found a workaround. I think it's crazy sexy cool.
To very briefly recap, here's the problem:
- Under normal class inheritance circumstances, calling a class method will execute the code at the youngest level (i.e. child class vs. parent) where a method definition can be found.
- When a class method binds an event to another method in the same class, it's binding the definition of that method within the same class. So if a class inherits from another that is binding events and does not override the method doing the binding, the event will be bound to the parent's copy of the target method, even if the child class overrides the bound method.
And here's the workaround:
Within the previously abstract definition of the event handler, we issue a call to itself, but with two subtle additions:
- We use the Me keyword to identify the method. Designer Help implies that this forces the method call to refer to a current class member, but that's not entirely accurate: it actually forces a reference to a current object member. In other words, even though the method being bound to the event is in a parent class, when it's executed it still knows it's inside the scope of an object typed to a child class, so calling Me.whatever executes the same method as defined in that object's class, not its parent class.
- We do a stack trace check to make sure that we don't cause infinite recursion. You see, if the bound method isn't overridden in the child class, Me.whatever only exists in the parent class, so it would keep calling itself over and over... stack overflow, Notes go boom.
For example:
Private Sub Inviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant, Columnvalue As Variant, Continue As Variant)
If Not(Me.isRecursive(Fulltrim(Split(Lsi_info(14), Chr(10))))) Then
Call Me.inviewEdit(Source, Requesttype, Colprogname, Columnvalue, Continue)
End If
End Sub The isRecursive method is actually defined at the very top level:
Private Function isRecursive (stackTrace As Variant) As Boolean
Dim stackCount As Integer
Let stackCount = Ubound(stackTrace)
If (stackCount > 0) Then
Let Me.isRecursive = (stackTrace(stackCount) = stackTrace(stackCount - 1))
End If
End Function The end result is a framework in which you can create a derived class that overrides only the events you wish to bind, then attaches to those events, either from within the class itself or via calls to its inherited attachEvent method:
Public Class ExampleDocumentBinder As DocumentEventBinder
Public Sub New (Source As NotesUIDocument)
Call Me.attachEvent("Querysave", Source)
End Sub
Private Sub Querysave(Source As Notesuidocument, Continue As Variant)
Dim newEdit As String
Dim editType As String
If (Source.IsNewDoc) Then
Let editType = "created"
Else
Let editType = "modified"
End If
Let newEdit = Cstr(Now) & " - Document " & editType & " by " & Me.getSession().commonUserName
Call Source.Document.ReplaceItemValue("AuditTrail", Fulltrim(Split(newEdit & Chr(13) &_
Join(Source.Document.GetItemValue("AuditTrail"), Chr(13)), Chr(13))))
End Sub
End Class When calling attachEvent, you can pass a single event name, a comma-delimited list of events, or an Array of event names. The second parameter is the object whose event you're binding; if you pass Nothing, it will use the object initially passed to the constructor. The entire framework (along with a few examples) is available for download.
By the way, you might have noticed that this workaround uses the infamous Lsi_info. This undocumented function is almost universally considered unsafe, but is generally acceptable to use in an unthreaded context... so you wouldn't want to use this technique in web agents that could be running simultaneously. But as you've no doubt also noticed, event binding is entirely specific to Notes UI contexts (with the exception of NotesXMLProcessor descendants... and I think we'll leave tackling SAX parser event binding for another day), so I'm not too nervous about using Lsi_info for this niche purpose. About an hour ago, Nathan mentioned he's been playing with CodeLock as a way to make this even safer, so you might be seeing further revision to this model post haste (On Event PostHaste From... tee hee, just kidding).
|
Workaround for LotusScript event binding
|
Crack open a tasty beverage and commandeer a comfortable chair... this is going to be a long one. In preparation for another Yellowcast episode, I've been playing with remote event binding in LotusScript. It's something I find myself using more and more frequently, but I decided I wanted to build a mini-framework to standardize how I approach it, as well as giving me something to provide to my loyal readers for instructional purposes if not ongoing usage. In the process, I've stumbled upon what appears to be a bug in how LotusScript handles overridden methods in derived classes when event binding is involved.
Perhaps I should back up a bit first. What is remote event binding? Simply put, it allows you to designate custom methods to be called when a LotusScript UI object's event is triggered. You're already familiar with this, though you might not realize it: in Domino Designer Help, check out the documentation for the NotesUIDocument class. Scroll past the Properties and Methods, and you'll see a list of familiar events. To briefly belabor the obvious, the reason they're so familiar is because they map directly to (a subset of) the events on the Objects tab when you're editing a form element in Designer. Every time you've added code to the Querysave Sub in a form, you've locally bound that event. When a user tries to save a document based on that form, the Querysave event of an anonymous NotesUIDocument instance is triggered, so the code you entered is executed. But this is actually just a convenient shortcut for binding to that event. There are a couple other ways you can achieve the same result using the oft-overlooked On Event statement.
The easiest is to define a new top-level function within the same element. For example, if you create a new Sub (say, myQuerySave) from within the global declarations of the form, you can bind that Sub to the Querysave event:
Sub Postopen(Source As Notesuidocument)
On Event Querysave From Source Call myQuerySave
End Sub In this case we're taking advantage of a local event binding (namely, the Postopen event) to get an automatic handle on the aforementioned anonymous NotesUIDocument and bind its Querysave event to our custom Sub. What does that gain you? Well... absolutely nothing, to be honest. You could have just entered the contents of the new Sub into the existing Querysave sub, and the results would have been the same. But this gives us an obvious illustration of how event binding actually works: when an event is triggered, every bound routine is called, and passed a predetermined set of parameters... which means each bound routine must have the exact same method signature (nitpick: technically the variable names can be changed but the sequence of data types must directly correspond). This is crucial to remember for later, so tuck that away for now while we delve into a slightly more advanced approach.
Imagine you've defined a custom class that may or may not be instantiated while a document is open. Perhaps it's a class defining the document's workflow, determining its current state, who needs to approve it based on who submitted it and a specified dollar amount, etc. Because you've moved beyond elementary development (pardon the assumption... if you've bothered to read this far, it's likely a safe one), you no longer use the standard input validation events, preferring instead to perform single-message validation in the Querysave or Querysend. After all, what user wants to get smacked in the face with a separate Msgbox for every nonconforming field each time something triggers a document refresh? Not to mention that typical input validation formulas completely preclude a notion of "Save As Draft", requiring the user to completely (and correctly) populate a document in order to save any of it. I once inherited a purchase requisition application that was losing the company money simply because its interface was obnoxious. Seriously. Users would try to create a requisition, and before the form had even loaded, "Smack! You forgot to enter a dollar amount!"... then when exiting any field (that's right, "automatically refresh fields" was enabled): "Smack! You forgot to enter a vendor name!" "Smack! You forgot to enter a shipping address!"
(These weren't the exact error messages, of course, just a rough approximation of user perception.)
Apparently (and unsurprisingly) people would get so annoyed that they'd give up, intending to try again later, but by the time they mustered up the courage to face the application again, the promotional discount period for the item they wanted to purchase had already expired, so we'd have to pay 10 or 15% more than they'd originally planned. Have I ever mentioned how much I hate standard input validations? Epic hate.
Uh... where was I? Oh yeah, event binding. So you've got a class that is only instantiated if the user is trying to actually submit the document for approval, and you want to trigger the field validation right before the workflow status is changed, because up until that point, who cares if they haven't filled out the required fields? So you put the validation code in a separate method of that class (because each method should only do one thing and do it well)... from anywhere within that class (including the constructor), you can bind the class method to the event.
Public Sub New(Source As Notesuidocument)
On Event Querysave From Source Call validateDocument
End Sub Why is this any better? A couple reasons. First, the code is never executed unless the class is instantiated, so you don't have to do any checking within the event handler itself to make sure it even needs to do anything. Second, and more importantly, the event handler has access to everything inside the class. You don't have to maintain a bunch of global variables or pass a bunch of local variables to the validation routine in order for it to know how to do its thing. It lives inside an object that's (hopefully) already storing what that routine needs to know and/or can hand off control to other methods of the same object, which in turn share the same capacity. Finally, if you had an entirely different type of class - one that you've instantiated numerous instances of... which you presumably wouldn't, in this example - each can bind a method to the same event, allowing you to define the functionality in a single location but take advantage of in each instance, potentially in different ways depending on what is unique to each instance.
One more implication to consider before we move on: because all bound event handlers are called with the same parameters, each by reference (as opposed to by value), any changes to a parameter passed to each handler is visible to any other handlers. For example, imagine we want to add another handler to the above example:
Public Sub New(Source As Notesuidocument)
On Event Querysave From Source Call validateDocument
On Event Querysave From Source Call updateHistory
End Sub
Not only are any changes made to the NotesUIDocument within validateDocument still intact when updateHistory is called, any events that pass a Continue parameter check its value between handlers and suspend all outstanding handlers if it is now False. In other words, in the new example, if validateDocument toggles Continue to False, updateHistory is never called, nor is the intrinsic handler (writing the document to disk). To expand this just a tiny bit more:
Public Sub New(Source As Notesuidocument)
On Event Querysave From Source Call validateDocument
On Event Querysave From Source Call updateHistory
On Event Postsave From Source Call notifyNextApprover
End Sub Just as you don't have to verify within updateHistory that validation was successful - because that routine wouldn't have been triggered if validation failed - you don't have to verify that the history was correctly updated before sending the approval request, because if it hadn't been, the intrinsic Querysave handler is skipped, which means the Postsave event never fires (NOTE: this assumes you're toggling Continue to False in your error handler block in each event handler). Your code is simplified, because you're allowing Notes to manage your event flow for you. The only downside is that anyone who has to maintain your code later will be confused by the absence of custom flow handling if they don't understand how automatic all of this is and will probably add in their own unnecessary conditional processing.
Okay, now let's take this even further. All of the above could be accomplished by defining the class directly in the form's declarations, but it's more likely that, for purposes of maintainability (per my earlier assumption), you defined it in a script library that's loaded by the form via a Use statement. Hence, the document knows about that code and can leverage all its yummy bindiness. Believe it or not, you can still bind to an object's events even if the design element that creates the object knows nothing about the code binding to its events. If you open a document via NotesUIWorkspace.editDocument, for example, you now have a NotesUIDocument handle on the document being opened, and can bind methods defined (or loaded) within the context from which the document was opened to the document's events (this memory scope overlap was a key factor in Nathan's Revolution). This could allow you, for example, to define standard audit trail logging in a centrally inherited script library and bind that behavior to every form in every application... without ever loading that script library in any of those forms. You wouldn't want to though, because then your audit trail logging only occurs if the document is opened in a way that allows the event(s) to be correctly bound (so you'd have to bind to the Queryopendocument event in every view to cancel the standard open and instead open the very document they were trying to open, but via a handle you can bind to... and, of course, doclinks bypass all of this). But flip that around: what if you want an event to be handled differently based on how the user opened the document? That's where this really comes in handy. Instead of using a form formula to load a completely different form, you can bind entirely different event handlers to the document depending on whether they opened the document via a doclink, an agent, a button, or simply double-clicked it in a view. I'll let your imagination run wild about all the possibilities this opens up.
So what's the problem? Well, as you might expect, all of this comes with some caveats in addition to those already described or alluded to. For one, it's extremely sensitive about memory scope. In fact, it works best if the bound event handler is declared globally... which is why it's best to bury this inside a class, so you can consolidate everything the handler needs to do and know inside the same object, instead of relying on a glut of global variables and top-level functions. But that's fine... we should be doing that for the bulk of our code anyway. What worries me is that I also seem to have found a flaw in class inheritance. Here's your opportunity to tell me what I'm doing wrong (okay, yet another opportunity). Consider the following, from a dummy script library:
Public Class A
Public Function attachEvent
Call Me.bindEvent()
End Function
Public Function bindEvent
End Function
End Class
Public Class B As A
Public Function bindEvent
Call Me.doEvent
End Function
Public Function doEvent
End Function
End Class Another library that loads the above library contains the following:
Public Class C As B
Public Function doEvent
Msgbox "Consider it done"
End Function
End Class Load that library into an agent, button, etc. and initialize a C instance (oops, poor naming choice... no, we're not messing with API calls here), then call attachEvent. B inherits from A, so it gets its attachEvent method, and overrides its bindEvent method. C inherits from B, so it gets the inherited attachEvent and overridden attachEvent, and overrides doEvent. As you would expect, you'll get a Msgbox. No problem. But when you introduce event binding into the mix:
Public Class EventBinder
Public Function attachEvent (Byval eventName As String, Source As Variant)
If (Source Is Nothing) Then
Set Source = Me.getContext
End If
Call Me.bindEvent (eventName, Source)
End Function
Private Function bindEvent (Byval eventName As String, Source As Variant)
End Function
End Class
Public Class ViewEventBinder As EventBinder
Private Function bindEvent (Byval eventName As String, Source As Variant)
Select Case Lcase(eventName)
Case "inviewedit":
On Event Inviewedit From Source Call Inviewedit
Case "queryopendocument":
|