Workaround for LotusScript event binding
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.
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.
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).








Comments
Posted by Chris Toohey At 18:15:28 On 07/31/2008 | - Website - |
Posted by Tim Tripcony At 20:26:06 On 07/31/2008 | - Website - |
Posted by Nathan T. Freeman At 20:35:41 On 07/31/2008 | - Website - |
{ Link }
... thanks.
Guess it's still better than Matter-Eating Lad though.
Posted by Chris Toohey At 23:18:31 On 07/31/2008 | - Website - |
you found a nice solution, but wouldn't it be even simpler to just seperate the binding and the overwritten method using the Template Methode pattern:
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 CallInviewedit
End Select
End Function
Private Sub CallInviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant, Columnvalue As Variant, Continue As Variant)
Call Me.Inviewedit(Source, Requesttype, Colprogname, Columnvalue, Continue)
End Sub
Private Sub Inviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant, Columnvalue As Variant, Continue As Variant)
'abstract function, will be overridden in a descendant
End Sub
End Class
Instead of binding the abstract method, I bind CallInviewedit, which in turn calls the abstract method, which gets overwritten by the subclasses.
But I admit, this solution isn't as crazy sexy cool as yours.
Thomas
Posted by Thomas Bahn At 05:06:15 On 08/01/2008 | - Website - |
Thomas
Posted by Thomas Bahn At 05:07:54 On 08/01/2008 | - Website - |
You could try to check the Typename in the Inviewedit method you give, but if it's not overridden, you'd still get the name of the derived CLASS.
I guess you avoid the possibility of recursion in the first place, which makes the locking unnecessary, so that's cool -- and certainly safer. Not as much fun, though.
Posted by Nathan T. Freeman At 08:20:18 On 08/01/2008 | - Website - |
Thomas
Posted by Thomas Bahn At 08:23:55 On 08/01/2008 | - Website - |
Posted by Nathan T. Freeman At 08:37:17 On 08/01/2008 | - Website - |
Posted by Tim Tripcony At 09:59:25 On 08/01/2008 | - Website - |
Public Const ERR_ABSTRACT_INSTANTIATION = 1000
Public Const MSG_ABSTRACT_INSTANTIATION = |An abstract class may not be instantiated as an object instance. |
Public Const ERR_ABSTRACT_METHOD = 1000
Public Const MSG_ABSTRACT_METHOD = |An abstract method must be overriden. |
Public Class EventBinder
Public Sub New()
Dim classname As String
If (Typename(Me) = "EVENTBINDER") Then Error ERR_ABSTRACT_INSTANTIATION, MSG_ABSTRACT_INSTANTIATION & classname
End Sub
Public Function attachEvent (Byval eventName As String, Source As Variant)
If (Source Is Nothing) Then Exit Function
Call Me.bindEvent (eventName, Source)
End Function
Private Function bindEvent (Byval eventName As String, Source As Variant)
'abstract function, must be overridden in a descendant
Error ERR_ABSTRACT_INSTANTIATION, MSG_ABSTRACT_INSTANTIATION & |Class = | & Typename(Me)
End Function
End Class ' EventBinder
Public Class ViewEventBinder As EventBinder
Public Sub New()
Dim classname As String
If (Typename(Me) = "VIEWEVENTBINDER") Then Error ERR_ABSTRACT_INSTANTIATION, MSG_ABSTRACT_INSTANTIATION & classname
End Sub
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":
On Event Queryopendocument From Source Call Queryopendocument
End Select
End Function
Private Sub Inviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant, Columnvalue As Variant, Continue As Variant)
'abstract function, must be overridden in a descendant
Error ERR_ABSTRACT_INSTANTIATION, MSG_ABSTRACT_INSTANTIATION & |Class = | & Typename(Me)
End Sub
Private Sub Queryopendocument(Source As Notesuiview, Continue As Variant)
'abstract function, must be overridden in a descendant
Error ERR_ABSTRACT_INSTANTIATION, MSG_ABSTRACT_INSTANTIATION & |Class = | & Typename(Me)
End Sub
End Class ' ViewEventBinder
Public Class ExampleViewBinder As ViewEventBinder
Public Sub New()
' Custome Instantiation code goes here
End Sub
Private Sub Inviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant, Columnvalue As Variant, Continue As Variant)
' Custom InViewEdit code goes here
End Sub
End Class ' ExampleViewBinder
Is this pretty much it, or am I missing something?
-Devin.
Posted by Devin Olson At 13:02:41 On 08/01/2008 | - Website - |
grrr
Posted by Devin Olson At 13:06:41 On 08/01/2008 | - Website - |
Let's try this again:
Public Const ERR_ABSTRACT_INSTANTIATION = 1000
Public Const MSG_ABSTRACT_INSTANTIATION = |An abstract class may not be instantiated as an object instance. |
Public Const ERR_ABSTRACT_METHOD = 1010
Public Const MSG_ABSTRACT_METHOD = |An abstract method must be overriden. |
Public Class EventBinder
Public Sub New()
Dim classname As String
If (Typename(Me) = "EVENTBINDER") Then Error ERR_ABSTRACT_INSTANTIATION, _
MSG_ABSTRACT_INSTANTIATION & classname
End Sub
Public Function attachEvent (Byval eventName As String, Source As Variant)
If (Source Is Nothing) Then Exit Function
Call Me.bindEvent (eventName, Source)
End Function
Private Function bindEvent (Byval eventName As String, Source As Variant)
'abstract function, must be overridden in a descendant
Error ERR_ABSTRACT_METHOD, MSG_ABSTRACT_METHOD & |Class = | & Typename(Me)
End Function
End Class ' EventBinder
Public Class ViewEventBinder As EventBinder
Public Sub New()
Dim classname As String
If (Typename(Me) = "VIEWEVENTBINDER") Then Error ERR_ABSTRACT_INSTANTIATION, _
MSG_ABSTRACT_INSTANTIATION & classname
End Sub
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":
On Event Queryopendocument From Source Call Queryopendocument
End Select
End Function
Private Sub Inviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant, Columnvalue As Variant, Continue As Variant)
'abstract function, must be overridden in a descendant
Error ERR_ABSTRACT_METHOD, MSG_ABSTRACT_METHOD & |Class = | & Typename(Me)
End Sub
Private Sub Queryopendocument(Source As Notesuiview, Continue As Variant)
'abstract function, must be overridden in a descendant
Error ERR_ABSTRACT_METHOD, MSG_ABSTRACT_METHOD & |Class = | & Typename(Me)
End Sub
End Class ' ViewEventBinder
Public Class ExampleViewBinder As ViewEventBinder
Public Sub New()
' Custome Instantiation code goes here
End Sub
Private Sub Inviewedit(Source As Notesuiview, Requesttype As Integer, Colprogname As Variant, Columnvalue As Variant, Continue As Variant)
' Custom InViewEdit code goes here
End Sub
End Class ' ExampleViewBinder
There, NOW am I getting it?
Posted by Devin Olson At 13:11:24 On 08/01/2008 | - Website - |
Ultimately, we still need some kind of recursion protection - either the stack trace analysis route Nathan and I took or the function delegation Thomas suggested. The reason is that the quirk we're trying to circumvent is that all events are bound to the local routine definition, not to the object's class definition, so in your example above, the event would be bound to the abstract Sub in ViewEventBinder even if they followed the instructions and overrode the handler for the event, so it would throw the abstraction error even if they did as they were told.
So you still need a way of determining if the method has been overridden in the descendant class, but because LotusScript doesn't support class introspection, there's no intrinsic way to ask. Nathan's stack trace analysis approach works because we tell the object to call the method it's already in; the first time it's run, the stack trace contains a single entry (the abstract Sub)... the second time it's run, it's either called the overridden method in the descendant class, or it's called itself again, in which case the stack trace contains two identical elements, so isRecursive returns True and the Sub exits. Thomas' function delegate approach works because you're binding to a function that just calls another, but since it's using the Me.methodName syntax, it will call the overridden definition if one exists, and an empty abstract definition if not. So both approaches "fail" silently if a descendant class binds an event without a defined handler. But you could do something like this:
Private Sub Queryopendocument(Source As Notesuiview, Continue As Variant)
This approach ensures that, if the method has been overridden, the overridden definition is executed, otherwise it throws an error but doesn't infinitely recurse.If Not (Me.isRecursive(Fulltrim(Split(Lsi_info(14), Chr(10))))) Then
Call Me.Queryopendocument(Source, Continue)
Else
'abstract function, must be overridden in a descendant
Error ERR_ABSTRACT_METHOD, MSG_ABSTRACT_METHOD & |Class = | & Typename(Me)
End If
End Sub
Naturally, how errors are handled, logged, and/or displayed is a matter of personal preference. I'm all for handling errors silently... don't scare the user with a cryptic error message that undermines their confidence in the application. On the other hand, overconfidence can be even worse: if an error occurs and the user isn't informed, they think everything is fine, only to find out later that something was supposed to happen but didn't. So if the user base is conditioned to report errors, and applications give them useful information to report, everybody's happy: they know something went wrong, but they also know how to obtain a resolution.
Posted by Tim Tripcony At 20:41:40 On 08/02/2008 | - Website - |
Problem one is, there is no keyword Abstract (or Static or Final for that matter). If we really wanted something to be abstract, we would call it out as:
Public Abstract Class Template
and you'd then not be able to compile something like:
Dim tmplt as Template
The compiler should throw a compile time error, as Devin writes it - "An abstract class may not be instantiated as an object instance."
So, really, LS doesn't do proper inheritance modeling. Part of real OOP is the ability to create interfaces, which is what the whole concept of abstract patterns is all about. An OO language without Interfaces is necessarily tightly bound and looses a lot of important flexibility that would lend it to lager considerations. Creating a framework, for example, becomes a rather specific implementation rather quickly. Try writing something the scale of SpringDAO in LS and you'll quickly run out of ceiling height.
The second problem is the event binding, or rather our expectation of it. It's working correctly but has no concept of Abstract or inheritance. Event binding is a run time scripting tool that provides a layer of control between the UI events and your code. It's handy for fine tuning behavior in a UI, but I think the scope has always been limited by design. Since we're artificially introducing the concept of an abstract method into LS, event binding can't be expected to understand it anymore than it can be expected to understand inheritance, which only half way works right.
If inheritance in LS was for real, you'd be able to inherit product classes and override methods therein. Last time I tried that it didn't work out so well.
Meh. It's late, my brain is now running on rabbit hole fumes, and I think we've verified what we knew all along. LS is good, but it's not that good. With the continuing convergence of JavaScript with the client, I see no reason it will ever be fixed either - not meaning to sound pessimistic, but why for? JavaScript will replace it and any effort to get Lotus Script to behave like C++ or Java would unltimatley be a waste of dollars for IBM.
A real solution would be to make the client future proof by allowing some form of aspect oriented run time compatibility so you can fire up the Notes client with any future JVM as the runtime, any future version of JavaScript as the native version, and the same for Domino - let the HTML / XHMTL doctype, WSDL, SOAP, et al never be a hard coded product level setting. And while I'm smoking crack**, open sourcing Lotus Script would be cool too.
** metaphorically
Posted by Jerry Carter At 02:14:48 On 08/03/2008 | - Website - |
I keep seeing/hearing people say that and I have no idea where they're getting that impression. Javascript hasn't advanced in the Notes client since version 6.
When we get Xpages in the client, the Lotus version of Javascript might become the scripting engine of choice -- but that's pretty opaque on how that might work at this point.
Posted by Nathan T. Freeman At 09:46:39 On 08/03/2008 | - Website - |
Posted by becomcs At 10:52:21 On 12/19/2008 | - Website - |