Lotusscript and Domino musings

 
alt

Lars Berntrop-Bos

 

Reaching Form and View event Nirvana in LotusScript

Lars Berntrop-Bos  December 23 2022 06:33:40 AM
Intended audience, why would I find this interesting?
This post accompanies https://openntf.org/main.nsf/project.xsp?r=project/Form%2C%20Subform%20and%20View%20event%20Nirvana
LotusScript developers who would like to make maintaining databases less work.  Some experience is helpful.  I do submit that learning about classes in LotusScript is beneficial anyway!

Especially if you have a database or set of databases where the code seems like a big old hairball and you would really like to update the code but starting from a clean slate is just not possible.  The presented method allows you to isolate the code from the Form, Subform and View design elements.

You then become much more flexible in refactoring, recoding, rearchitecting, changing the plumbing bits that are used everywhere but you put off changing because tinkering with it causes so many dependent scripts to bug out.


Introduction

LotusScript is an easy language, and Notes applications can be easy to build.  But updating the scripts can be a chore if you put your code in the Forms and Subforms.  I present a method that will free you from having to recompile and save all the formsm, subforms and views when code in libraries changes.

Yes, the excising of code from the design elements is work.  But I think very rewarding work.

Example: initially, I had just done the Forms and Subforms part.  But as I was putting together a sample database to accompany the article, I thought:  Let's see how much effort is needed to develop the new base class for View events.  It turned out to be over very soon, copy and pasted the FormEventsBase, removed the parts not needed, wrote the parts that were extra... Then I just included it in the sample database.  I'll be incorporating it into the hairball that started it all next week.


Background

You probably recognize this scenario: you need to update an important app, but as time goes on you realize that while the application has a lot of good functions, it is also a hairy ball of code.  Code that is spread out and has lots of duplicated routines, sometimes the same, often a bit different.  Code in Forms, Subforms, Fields, Buttons, View events, Form events, you name it, it can have code.


Updating the codebase and dragging it firmly into more modern times can be quite a challenge, if you want to modify the existing app and do not have the time or budget (or both) for a full back-to-the-drawing-board rewrite.  You modify an eensy-teensy call down in the guts, move a much needed method to better place in the object hierarchy and boom you need to recompile everything.


While Recompile All is built in, I do not like it.  All the design elements are changed, even if they did not need it.  And they all get the same date, you lose a sense of history.  Plus it is time-consuming.  So I struggled along for a while, but then I stumbled across a great method to drastically reduce the number of design elements I need to save even if I do significant rework on the Script Libraries.


It is a big evolution of a strategy I already used for decoupling Agent properties form the code.  Especially scheduled agent have a schedule, can be enabled or disabled, have a log, all stuff you would like to treat separately from the code it executes.  I do so by coding the agent to use one Script Library and call one routine in the library.  After you've done that, you can change the Library to your heart's content and do not need to touch the agent again.  Which means that scheduling and the Agent log also stays intact on a Design refresh.


The new method extends this philosophy to Forms and Subforms (views are next).  Because Forms and Subforms also have Fields and Buttons, we need three fixed subroutines instead of one, but with that we achieve decoupling, and after separating code from the Forms and Subforms we can also change the code used by thee Forms and Subforms without Edit and Resave or Recompile!

History, why recompiles are needed

I inherited the maintenance for a complex workflow app, using a main Task form and 100+ subforms/views/actions to implement various workflows in the company.  Updating the code was very tedious, because any structural changes caused close to a hundred dependent design elements needing an edit and recompile,

The reason for that is that if the call signature of a Public method or property changes, like the order and type of parameters, or the structure of an object, the calling code needs to be recompiled so it uses the correct call signature.
If you have many dependent design elements, this becomes time consuming.

Model of the old way

If you change a class or routine in General, EVERYTHING needs to be edited and resaved: each subform and each form. This is because all the code uses all the different paths to the lower libraries, represented by the little blue arrows in this diagram.  Code is all over the place.

Image:Reaching Form and View event Nirvana in LotusScript


Solution for the compile problem

To decouple Forms and Subforms, you write an interface Script Library for use by the Forms and Subforms with only three Public Sub methods. it can Use whatever other libraries you need, and whatever Private stuff you need, but the interface Libary must have a static public Signature for the method to work.

In the Forms and Subforms that use the interface Script library, you can only use those three methods.  All other code is accessed via the interface calls.


Model of the new way

The Form and Subforms have only skeleton code, and can only call one of the three methods in libTaskForm:

The only calls inside the Form box are calls to the three allowed public methods in libTaskForm.  libTaskForm and below are free to call whatever they like.

Image:Reaching Form and View event Nirvana in LotusScript
An advantage of the new way is that the code now lives in and can profit of other code in the Library, generally making life much more fun for the maintaining developer!


Implementation

Create a Script Library for the form and its subforms.  It is allowed three Public Subs:
  • Public Sub butClick(butID As String)
  • Public Sub fldEvent(eventID As String)
  • Public Sub taak_QueryOpen(formName As String, Source As NotesUIDocument, Mode As Integer, isNewDoc As Variant, Continue As Variant).
 
The task form and it's subforms are only allowed to call one of these three subs.  If you follow this rule, the interface to the Script Library stays constant from the perspective of the Form ans Subforms.  Thus even if the code signatures change in Script Libraries further down, a resave/recompile can be avoided.
For each Form / Subform / View you create a subclass (

Using the
On Event construct, the QueryOpen event also sets handlers for the other Form events, like for example a QuerySave.  This also allows for events to be added later by editing the Script Library, no edit of the Form or Subform is needed.

Important implementation detail concerning the use of Const

You may wonder why the sample database does not Const statements for the Form / Subform / View / butID / eventID names.  The reason is that using them breaks the concept of having a fixed interface from the Form / Subform / View design elements to the first library they depend on.  If you add a new Const, that constitutes a changed interface, thus needing to edit and save the Design elements => not cool.

Const use is fine between Libraries, but using them above the ScriptLibrary layer will burn you.


Guru meditations

If you want to get really fancy you could even set and unset handlers dynamically!  That needs a big brain though....

It would be even nicer if the
On Event construct could also be used to attach handlers to the Button events (an action is a Click event of a Button) or Field events (like for example OnChange).  That would save on form size on complicated forms (no more storage of Source code and compiled LS objects for actions and Fields) and would make the code storage cleaner.  It could then be parceled out in the different classes that already exist for the Form and Subform event handling code.  The problem I butted my head against is obtaining a handle to the Fields and Actions.  I cannot get a handle on all the Field and Button objects like with the single QueryOpen event enabling setting all the Form event handlers.  For Buttons or Fields I'd need to populate them all with code which is what I am trying to avoid.  Please drop me a line if you know how to get a handle on the Button and Field objects!
The base class, classForm

Image:Reaching Form and View event Nirvana in LotusScript
This is the Public interface of the base class, classForm.  Not very exciting.

The interface including the Private parts:

Image:Reaching Form and View event Nirvana in LotusScript
This shows more guts.  Items of Note: The OnSize sub is an Event that is only available on Notes 12+  I have no experience with it, so I will ignore it for now.  The other Query... and Post... subs are hooks for those events that SetHandlers uses to tie those events to.

The way it works is that you need to define Subclasses in the Library using libFormEvents.  One subclass for the form and one subclass per sub (or less, I've had occasion where I could handle several slightly differing subforms with one subclass).

For the subclass to work, it needs to override ErrInfo, SetHandlers and OnEvent.   If you forget, an error will be thrown.

Subclasses
for Forms and Subforms
The Task Form and its Subforms all have this in the (Globals)Task (Options) section:
Option Public
Option
Declare

Use
"libTaskForm"



No further declarations or routines.

The Task (Form) is almost bare.  The (Options)  section only has an
Option Declare


And only the QueryOpen event is populated:


Sub Queryopen(Source As NotesUIDocument, Mode As Integer, Isnewdoc As Variant, Continue As Variant)

task_QueryOpen "Task", Source, Mode, isNewDoc, Continue

End Sub


All the other events are empty, if they need code then the QueryOpen call will connect that code dynamically.  Read on to see how. All the events can contain code if you need to, without saving or recompiling the form.  If necessary, changing the form events can even be done at runtime.

libTaskForm has only three Public entrypoints, task_QueryOpen, butClick, and fldEvent.  Every LotusScript button or Action calls butClick with an ID, every LotusScript field event calls fldEvent with an ID, and all the Form and Subform form events call are handled by a call on each Form or Subform:
task_QueryOpen , Source, Mode, isNewDoc, Continue

Converting the Forms and Subforms to the new system

The Form events are handled in private subclasses in libTaskForm, and the buttons and field events need to be handled by the butClick and fldEvent subs.
The code from the events in a Form / Subform needs to be moved to the subclass created for the Form / Subform, and you need to walk the design element and move all the LotusScript code to routines called by the butClick or fldEvent methods in libTaskForm.  Take care to look for routines hiding inside the Form or Global section.

task_QueryOpen Magic

Now we get to the inner workings.  To make the solution available in different Forms, we first need a foundation.  This is mostly in the library "libFormEvents", it contains the base class with the mechanics and plumbing.  The technique we use is subclassing, that is in libTaskForm we make several classes, one for the form and one for each subform (or set of nearly identical subforms).   And one extra class in between the base class and the other classes, to provide extra services to all the subclasses for error reporting.  Fun fact:  normally such a refactoring would have required me to edit and resave all the forms and subforms, more than 100 in total.  But because I already implemented the 3-call solution, only the involved Script Libraries libFormEvent and libTaskForm needed to be changed.  

The trick is you divide the work into several parts, and then override the parts that need to be different.  That might appear to need an overly convoluted base class that has several routines doing nothing, but those exist so the behavior can be modified in a subclass, without having to duplicate the generic plumbing parts.

form_QueryOpen looks like this:


%REM

Sub form_QueryOpen

decode formName and instantiate the appropiate handler class

%END REM
Public
Sub form_QueryOpen(formName As String, Source As NotesUIDocument, Mode As Integer, isNewDoc As Variant, Continue As Variant)

Const proc = "form_QueryOpen" ' name of the current routine, because GetThreadInfo only does ALLCAPS

On Error GoTo errLog  ' Turn on error trap

initGeneral ' call a routine in a general library to setup global variables, read application config, etc

If nodebug Then On Error GoTo errLog Else On Error GoTo 0  ' noDebug indicates the LotusScript debugger is NOT active

If IsElement(formHandler(formName)) Then  ' catch double use of formName
 
Error 1001, "Aborting QueryOpen, formname already used: " + formName
End
If

Select
Case formName ' decode formName
 
Case "Task"  ' main form
         
Set formHandler(formName) = New classTaskForm(formName, Source, Mode, isNewDoc, Continue)
         
 
Case "task-order"  ' subform
         
Set formHandler(formName) = New classTaskForm_order(formName, Source, Mode, isNewDoc, Continue)
         
 
' other subforms go here

 
Case Else
         
Error 1001, "Aborting QueryOpen, unknown formname supplied: " + formName
End
Select

exitSub:

Exit Sub

errLog:

Continue = False  ' Stop opening the document
Call
logErrorStack(Error$, proc, Erl)  ' log the error stack and inform the user
Resume
exitSub

End Sub ' task_QueryOpen


An aside on programming style: Error logging, Option Public, Globals, a config class

Now that we have separated the code, we can recode the application and we only have to save the ScriptLibraries.  I adhere to several additional methods to make my life easier:


1.  No Option Public unless leaving out would be silly, like a Library that only defines constants.  But even then I have a hard think.

Pro: new methods and globals are Private by default, lessening the chance of interference.

Con: The Eclipse LotusScript editor does not embellish the icons properly in the navigator.  Case open with HCL.


2. Use an error trapping construct with several moving parts everywhere.  Every method has a
Const proc = "sampleProcName" because I dislike having my method name corrupted into ALLCAPS by GetThreadInfo. Then first level entry points (for example the Public methods in libTaskForm) do If noDebug Then On Error GoTo errLog others do If nodebug Then On Error GoTo bublUp
noDebug is a Public Global in Script Library lsAgentLog.  It is set  by execting a Stop statement and timing the execution..  If the debugger is active, then a long time indicates a human is watching the code execution and noDebug is set to False.  So when an error occurs, the debugger pops up.

ErrorLog writes a log document and shows a messsage box.

BubbleUp executes an Error statement, optionally with extra information avalable in the specific routine that is doing the BubbleUp.  This way you get an annotated call stack.


3. The lsAgentLog Library also makes the NotesSession object available, and some others.  The library lsconst is also included (thanks to Andre Guirard for the idea!).

4. libGeneral has a config class making available config values, other databases used in the application etc. It also has generic helper stuff.


Weaving the classes

The way to make the Form events more dynamic is the use of the Lotusscript "On Event" construct. The base class has a SetHandlers that looks like this:


     ' mSource is the NotesUIDocument passed as Source in the task_QueryOpen call
 
On Event PostOpen From mSource Call PostOpen
 
On Event PostOpen From mSource Call PostOpen
 
On Event QueryModeChange From mSource Call QueryModeChange
 
On Event PostModeChange From mSource Call PostModeChange
 
On Event QueryRecalc From mSource Call QueryRecalc
 
On Event PostRecalc From mSource Call PostRecalc
 
On Event QuerySave From mSource Call QuerySave
 
On Event PostSave From mSource Call PostSave
 
On Event QuerySend From mSource Call QuerySend
 
On Event PostSend From mSource Call PostSend
 
On Event QueryClose From mSource Call QueryClose

The effect is that when the occurs in mSource, the specified routine is called.  A fortunate aspect is that this can be a Private sub, invisible from the Form!  So no recompile needed if you later decide a QuerySave must be added to enforce a validity check.

Once SetHandlers has been called in QueryOpen, when one of the connected events fires, the listener is fired. For example, this is QuerySave in the base class:


      %REM

              Sub QuerySave

             

      %END REM

      Private Sub QuerySave(Source As NotesUIDocument, Continue As Variant)

              Const proc = cN + evQS

              If noDebug Then On Error GoTo errLog

             

              Set mSource = Source

              mContinue = Continue

              OnEvent evQS

              Continue = mContinue

      exitSub:

              Exit Sub

      errLog:

              Call logErrorStack(Error$  + errInfo, proc, Erl)

              Resume exitSub

      End Sub ' QuerySave

This routine is in the base class, and stores the passed in parameters in properties in the base class to act upon in the OnEvent method.  If you look back up to the example subform subclass, you see it (OnEvent in the Subclass 'classtaskForm_order') will be called from the method 'QuerySave' in the base class.

Some parts of the base class must be overridden to make it all work.  To protect myself from programming errors, the base class throws an error if a method that needs to be overridden is forgotten.

Subform subclass example

This subclass handles two subforms, "task-order" and "task-order-bell"


%REM

Class classTaskForm_order

"task-order", "task-order-bell"

%END REM

Class classTaskForm_order As classFormTask

     
 
Public Sub New(FormAlias As String, Source As NotesUIDocument, Mode As Integer, IsNewDoc As Variant, Continue As Variant)
 
End Sub ' New

      %REM

              Sub setHandlers

             

              form specific event handler setup

      %END REM

      Private Sub setHandlers

              Const proc = "classTaskForm_order::setHandlers"

              If noDebug Then On Error GoTo bublUp

             
         
  Select Case mFormAlias

Case "task-order", "task-order-bell"

                      On Event QuerySave From mSource Call QuerySave

                             

                  Case Else ' catch typo's!

                      Error 1001, "Aborting setHandlers, unhandled formname supplied: " + mFormAlias

              End Select ' mFormAlias

              Exit Sub

     

      bublUp:

              Error Err, addBubblEr(proc, Erl, Err)

      End Sub ' setHandlers

     
 
Private Sub OnEvent(eventName As String)

              Const proc = "classTaskForm_order::OnEvent"

              If noDebug Then On Error GoTo bublUp


           
Dim doc As NotesDocument

              Dim msg As String

              Dim lstFields List As String

             

              Set doc = mSource.Document

              Select Case eventName

                      Case evQO ' NOP, but must detect for error trap

                      Case evQS

                              Select Case mFormAlias

                                      Case "task-order"

                                              If gs(doc, "taskTitle") <> "Beëndiging contract" Then

                                                      lstFields("Afleveradres") = "olaAdresAflever"

                                              End If

                                             
                                 
Case "task-order-bell"

                                              lstFields("Afleveradres") = "olaAdresAflever"

                                              If gs(doc, "cashAanvBetaling") <> "" Then

                                                      lstFields("Factuuradres") = "olaAdresFactuur"

                                              End If

                                              ringBell

                                             
                         
End Select ' mFormAlias

                              taskSF_QuerySave mSource, mContinue, lstFields

                             
                 
Case Else

                              Error 1001, "Aborting onEvent, unhandled eventname supplied: " + eventName

              End Select ' eventName

              Exit Sub

             
  bublUp:

              Error Err, addBubblEr(proc, Erl, Err)

      End Sub' OnEvent

     
 
' classTaskForm_order

End Class


Sethandlers connects the events to the methods.  I validate the formAlias so to catch any typos I made.  The empty New is necessary because the base class has a New method with parameters.  OnEvent overrides the base class event handler, this is where the events are actually executed.

Events and buttons in Views

As an example of the freedom this method allows, in about two hours I adapted the same method to handle View events.  For that I also renamed the base libraries, in each case I only needed to save a dependent Library and the application ran again, no Form, Subform or views saves needed.  Very liberating!
The sample database has the implementation of both the Form/Subform and an example for the View design element.

Challenge for the inquisitive

While constructing the sample I noticed I forgot to include some form events.  Can you find the ones that are missing?


Recent Entries

    Archives