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.
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.
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
This is the Public interface of the base class, classForm. Not very exciting.
The interface including the Private parts:
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
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 %END REM 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 exitSub: Exit Sub errLog: Continue = False ' Stop opening the document 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
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 %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 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 Const proc = "classTaskForm_order::OnEvent" If noDebug Then On Error GoTo bublUp
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 lstFields("Afleveradres") = "olaAdresAflever" If gs(doc, "cashAanvBetaling") <> "" Then lstFields("Factuuradres") = "olaAdresFactuur" End If ringBell taskSF_QuerySave mSource, mContinue, lstFields Error 1001, "Aborting onEvent, unhandled eventname supplied: " + eventName End Select ' eventName Exit Sub Error Err, addBubblEr(proc, Erl, Err) End Sub' OnEvent 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?
- Comments [1]