Search

What the Quote?

"We thought we'd be brilliant and just copy your code, and your code said, 'Yeah, you're funny'."

Monica Ferrante

"Let me show you something that'll melt your face off like a Nazi!"

Chris Toohey

"It's unlucky to be superstitious."

Paul Hannan

« Possible bug in LotusScript event binding - part 2 | Main| Mozy: cheap backup with unlimited storage »

Workaround for LotusScript event binding

Category lotusscript
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:
  1. 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.
  2. 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).

Comments

Gravatar Image1 - So, really we are talking about two problems.

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

Gravatar Image2 - So if I mix your stuff from yesterday's post in with Thomas' stuff, and add a little bit of my personal style, I come up with this:

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.

Gravatar Image3 - "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"

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.

Gravatar Image4 - GROWL Emoticon - Managed to screw up the correction.
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?

Gravatar Image5 - Hi Tim,

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. Emoticon

Thomas

Gravatar Image6 - Thomas, how is that not exactly the same solution, except with an extra method? You still have to find a way to ensure that the inviewedit method is overridden. But the essence is knowing that a parent method that invokes Me.blah will

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. Emoticon

Gravatar Image7 - My solution is just too avoid the recursion (and therefore the LSI_info and code locking issues). Nothing more.

Thomas

Gravatar Image8 - ... Brainiac 5 is - from what I can tell - a feeble anti-social geek with mommy issues that doesn't think about the consequences of his actions while enjoying the company of a Super-Girl fembot.

{ Link }

... thanks.

Guess it's still better than Matter-Eating Lad though.

Gravatar Image9 - Sorry for my late comments, but why parentclass..queryopen syntaxe isn't suffisient?

Gravatar Image10 - Trackback { Link }

Gravatar Image11 - @1 - Nah, man... you're Brainiac 5. Emoticon

Gravatar Image12 - Sh*t, the UBB code for code blocks doesn't work... Emoticon

Thomas

Gravatar Image13 - Batman to your Robin - wtf does that make me, Aquaman?!

Gravatar Image14 - Public Const ERR_ABSTRACT_METHOD = 1010
grrr

Gravatar Image15 - All of this, of course, was simply so I'd have an excuse to refer to Toohey as Aquaman from now on. Emoticon

Gravatar Image16 - @Nathan, he's right: that's much cleaner. We just bind to a function delegate (similar in some respects to how JavaScript handles this), which calls the real function (okay, fine, Sub) from within the object context. So if the object's class overrides that Sub, that code is executed... if not, it just executes the empty Sub in the parent class. Might not be as sexy as conditional execution based on real-time stack trace analysis... but it's a better pattern. Emoticon

Gravatar Image17 - @13, Almost... I dig the custom error throwing, but we don't necessarily want to force descendant implementations to override each event they try to bind to... we want to shield them from explosions if they choose not to override the event handler but attempt to bind to the event anyway. Throwing the error means that the caller routine must handle the error in order to prevent the user from seeing a message that doesn't tell them what they did wrong or should do differently (though the way you're formatting the message would tell the developer exactly what they need to do differently). But another weird thing about event binding is that the bound Sub is called directly, so there is no caller routine. The error must be handled within the body of the event handler; it cannot be "bubbled up" to a higher layer, because there's no code calling the Sub... the Notes client is calling it directly because a bound event was triggered.

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)
    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

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.

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.

Gravatar Image19 - This post is very good. Keep Sharing.

Gravatar Image20 - When we get X pages 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.

Gravatar Image26 - 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.

Gravatar Image32 - The code that you have written is excellent. I really enjoyed that a lot.

Post A Comment

:-D:-o:-p:-x:-(:-):-\:angry::cool::cry::emb::grin::huh::laugh::lips::rolleyes:;-)