Lotusscript and Domino musings

 
alt

Lars Berntrop-Bos

 

Tuning the Notes client backend for fun and profit

Berntrop Admin  May 22 2025 04:21:39 PM
After spending some time making the Eclipse RCP side run better, we need to look at the Notes client backend.

Due to the fact that the Notes backend shares a lot of code with the Domino server, there are some surprising possibilities!  It turns out there many "server" notes.ini settings that also work in the Notes client.

Before I discuss using Domino settings in the Notes client, I want to go over some settings that will make debugging easier.

The first of those settings is
; Log Status messages in log.nsf
LogStatusBar=1
That will cause all the messages that appear in the statusbar to also go to the log.nsf.  Sure that will generate mor log documents, but those status messages contain a lot of useful information.

Another is
; Log setting, keep longer
Log=log.nsf, 1, 0, 9999, 30000, 0
This change sets the number of days to keep the log documents to 9999.  The local log.nsf is quite modest in disk space use, and i like to be able to back a bit.
You used to be able to influence the size of the log documents, but around 11 or 12 30000 was blessed as the maximum value.  I noteced because i was using a policy to set the Log to a bigger value, and a small war ensued as the policy set the larger value and code inside the client was resetting the value.  A side effect of that was that the notes.ini got corrupted, as evidently there is not one proces responsible for writing to the notes.ini file. And as soon as you have empty or corrupted lines in your notes.ini, startup times of the Notes client tank, taking minutes to start instead of seconds.  If you recognize this, please open a support case with HCL.

Notice how I comment on the setting by starting the line with a semicolon ';', recommended!

Net I like to keep the Console.log as well, and the Domino settings work fine:
; ConsoleLog settings
Console_Log_Enabled=1
Console_Log_Max_kbytes=100000
Debug_ThreadID=1

Another tip originates from Panagenda's interesting webinars about Notes performance.  After every Notes or DOmino install, patch or update, I make sure to run a "compact -ODS -*".  This makes it so all the nsf, ntf and mail.box files are using the latest On-Disk-Structure, abbreviated to ODS.  I think it is telling that this is fourth column in the admin client Files tab.  I have had instances (ok itwas in 8.5 days) where having ntf templates in the default old ODS meant signicant performance loss in design refreshes on a Domino server on iSeries.
If you want to profit  the latest developments in io and diskaccess technology, you need the latest ODS.  So a no brainer to do. Since the -ODS parameter only does the actual copy-style compact if an ODS upgrade is needed, I usually do an online (meaning I run the command while Domino or Notes is running), then if dtabases were skipped because open, quit Domino or Notes and rerun the compact -ODS in offline mode.

Now let's turn to directories.
I like the temp files to be put in a known place, not in the Windows Temp or Notes Client Data directory:
 
; NotesTemp
Notes_TempDir=C:\HCL\Notes\dirTemp\

As you can see, I like to put my Notes program and Data directories NOT under 'Program Files', with or without (x86).  For starters, a space in the path needs you to use quotes on the command line.  And by default non-admin users cannot write to files stored under Program Files, which is a pain if you have the Data directory there.  Also makes it very easy to exclude the HCL directory from Windows Search indexing.  Windows indexing is a pain, and not very applicable to Notes programs and data.

One of the first Domino settings I discovered date back to R5 days.  I had a 486 laptop, running NT4.  One of the products had a database containing information for a large number of Compaq products.  This databse was often opened for reference, and Designer was not as stable as it is these days.  Especially if you're still learning the ropes and dive in and out of Designer help to look up Notes LotusScript product object properties and methods.  If Designer crashed, a consistency check would ensue , leaving me to twiddle my thumbs and do some coffee rounds for the office.

Unless: you enabled Transactional logging in the client.  For a bit of disk space, the excellent IBM server technology introduced in R5 called Transactional logging speeds up the startup considerably!
So ever since, I have been running my Notes client with transactional logging on.  With the settings adjusted for the Notes client:
; Transaction Log settings
; create new style block-aligned transaction log
Create_R85_Log=1
;  automatic fixup
TransLog_AutoFixup=1
;  do not use all the space for transaction logs
TransLog_UseAll=0
; Circular style (Cirular=0, Archival=1)
TransLog_Style=0
; 1=Favor runtime (fewer writes to transaction log, more changes in memory), 2=Standard, 3=Favor recovery time (more writes to the transaction log)
TransLog_Performance=3
; Transaction Logging enabled
TransLog_Status=1
;  Size of Transaction Log
TransLog_MaxSize=256
; Location of transaction log files
TransLog_Path=C:\HCL\Notes\dirTransLog\

Another setting concerns the buffer pool.  Daniel Nashed published that the Notes client by default keeps a very small one, so:
; BufferPool (so it is not only 8MB)
NSF_Buffer_Pool_Size_MB=128

You can put Full text indexes in a separate directory, making it easier to do maintenance and separating out the clutter.
; FullText Index Dir
FTBasePath=C:\HCL\Notes\dirFT\

On the suject of indexes: a while ago, Domino gained the ability to store indexes outside the nsf file.  This feature is also known as NIFNSF.  It needs Transactional Logging to be active.  And lo and behold, this also works on the client!
; NIFNSF
NIFBasePath=C:\HCL\Notes\dirNIF\
Create_NIFNSF_Databases=1
NIFNSFEnable=1

Caveat: just as on Domino, a database needs a special bit set, be on a new enough ODS, and if not created with the bit set needs a copy-style compact to actually move the indexes out of the database.  A nice side effect is that if you make a backup of the databases, having the full text and view indexes in a separate directory makes the backup smaller and faster, no need for a separate compact -d to drop the indexes.

I'm curious what you think, comment here or find me on the OpenNTF discord https://discord.com/invite/jmRHpDRnH4 !

Unshackle Domino Designer by modernizing jvm.properties

Lars Berntrop-Bos  May 1 2024 07:16:12 AM
The Notes Standard client and Domino Designer run as an Eclipse RCP application.

The Eclipse RCP application, runs on a JVM, short for Java Virtual Machine.


The Java Virtual Machine utilizes memory to do its work, called a heap.  There are lots of gears and knobs that go into setup and running of the machine, and key ones are configured in the file 'jvm.properties'.


That file lives in a subdirectory of the Notes client Program Directory: /framework/rcp/deploy/jvm.properties.

These are fed into the Equinox launcher via the command-line. You can inspect said command-line using the excellent
Process Explorer by Mark Russinovich.  Get the suite containing this essential tool and others here: https://learn.microsoft.com/en-us/sysinternals/downloads/sysinternals-suite .  You can see the command line by inspecting the properties of the notes2.exe process.   It is surprisingly long

Warning!

Files in the Program directories are not writable for non-administrative users.  So in R12, HCL created a way to customize this via the notes.ini, via WCT_vmarg settings.  But that mechanism does not allow you to REMOVE a setting, llike I have done in the examples.  The mechanism makes a copy in [DataDir]/workspace, and applies the WCT_ modifications.  If you edit the jvm.properties file, after some time your changes will be overwritten.
  AVOID!!

Keep versions

Keep a history of versions and what you tried to accomplish and what the results were.  This also enables a quick fix if an edit cripples Notes.  I once managed to us a FF (0x0C) character instead of a line end, and then Notes would not start anymore


Location of jvm.properties

The jvm.properties file is located in the [Program directory]/framework/rcp/deploy

For a 32bit Client 11 - 12.0.2 on Windows, the default program directory is C:/Program Files (x86)/HCL/Notes/ => C:/Program Files (x86)/HCL/Notes/framework/rcp/deploy
For a 64bit Client, 12.0.2 64bit and 14 onwards, the default program directory is C:/Program Files/HCL/Notes/ => C:/Program Files/HCL/Notes/framework/rcp/deploy


To avoid the directory-with-a-space and ease of use, I've grown the habit of using C:/HCL/Notes.  Also very handy if you want to run commandline, either offline or with the Notes client open.

If you look at the jvm.properties file, there is an IBM copyright banner, where the first year is 2006.
But some of the settings have older roots.  Some of these settings seem inappropiate in 2025...

I have been tinkering with jvm.properties for years, especially since I use Domino Designer a lot, and the default settings need adjusting.


Default settings
For reference the different default jvm.properties of the different versions.

Default settings for 12.0.1 FP1,  32-bit client (last client usable for developing LotusScript for 32-bit clients)
jvm_12.0.1_designer_default.properties

Default settings for 14.0,  64-bit client
jvm_14.0_designer_default.properties

Default settings for 14.5,  64-bit client (Early Access 3)
jvm_14.5_designer_default.properties


Annotated and tuned settings
Annotated and tuned jvm.properties files.

Annnotated and tuned settings for 12.0.1 FP1,  32-bit designer client (last client usable for developing LotusScript for 32-bit clients)
jvm_12.0.1.properties


Annnotated and tuned settings for 12.0.1 FP1,  32-bit multi-user client
(all clients use the same shared class file)
jvm_12.0.1_multiuser.properties


Annnotated and tuned settings for 14.0,  64-bit client
jvm_14.0.properties

Annnotated and tuned settings for 14.0,  64-bit multi-user client
(all clients use the same shared class file)
jvm_14.0_multiuser.properties


Annnotated and tuned settings for 14.5,  64-bit client (Early Access 3)
jvm_14.5_designer_default.properties


About the tuned settings
Shared classes cache size
All versions specify a significantly larger compiled Java shared classes cache.  For this to take effect, the existing cache file needs to be DELETED for the size change to take effect.  There are several settings like a change in AOT cache, storing line numbers or not and others that also need a cache delete to take effect.  Lookup the settings in the documentation ( https://eclipse.dev/openj9/docs/x_jvm_commands/ ) to see which settings need a cache reset.

Shared classes cache location
By default, the cache lives in [DataDir]/workspace/.config/org.eclipse.osgi


The location is set with the jvm.properties setting jvm.shareclasses.loc=


Shared classes cache multiuser optimization
On a machine where a multi-user Notes client is installed, you can optimize disk space usage by specifying a fixed location accessible to all potential users for the client.  The location needs to be accessible and writeable for all the users for the Notes client to be able to start.
For example: C:/ProgramData/HCL/Notes/org.eclipse.osgi
That is what I chose in the multi-user example files.
This way only one big cache file is needed. and will save 300MB per user beyond the first one.  This also works on VDI solutions like Citrix, and is the reason the singleJVM keyword was removed.  Tip from Panagenda webinars about optimizing Notes on Citrix / VDI.

The shared classes cache is essential
I changed nonfatal to fatal so the Notes Client fails if an error occuurs with the shared classes instead of starting really slow. You may decide differently.


General tuning tips
I recommend documenting your changes using the # character to make comment lines, keeping the defaults but commented, and documenting WHY you set or unset a setting.  Future you will thank you.
Keep versions.

Turn on the heap display to see what effects this has on the heap.  You can do this via preferences in Domino Designer, and via an edit of a file in the Client by adding SHOW_MEMORY_MONITOR=true to [datadir]/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs, tip published by Remco Angioni:
https://www.angioni.nl/2020/11/15/show-heapsizes-in-your-hcl-notes-client/
Note that the text of the inserted line is case sensitive!


Some significant parameters
-Xmx maximum heap space
 Maximum heap space tthe JVM can use. 512m (denoting 512 megabytes) by default.  Opening a simple database in Designer 14.5 puts the heaop at 777m.  Opening the same db in 12.0.1 Designer puts the heap at 312m, but then that has a max heap of 768m due to 32-bit constraints.
IBM has advised to put this on 1024m, but if you live in Designer all day like I used to, 32-bit Designer would become unstable and crash after a while.  If checking allocations, the actual amount of memory in use by Eclipse would slowly grow more until it hit the hard limit of 2 GB, at which point the process crashes.  768m is a limit which I arrived at through trials to allow for a stable experience.


On 64-bit Designer, you can set a 4GB or 8GB heap, allowing much breathing room, see the 14.5 jvm.properties file.  Then when you open the design for a new db, it opens nearly instantaneous instead of the lengthy process caused by the cramped heap of 12.0.1  32 bit Designer.


-Xms minimum heap space
 Amount the heap starts with.  By default 8m, which is tiny for an application the size of Notes.  You might think to make it a lot bigger, nut that skips an optimization I once read about on IBM developerworks.  The idea is to set a size large enough so the intial setup bumps into a limit just after most initial processing has been done.  The goal is to evict objects from the heap that were only needed for setup, while the Grabage Collect (GC) does not yet have to deal with a large old heap and can quickly finish.

In my experience, 128m is a nice value for that. The HCL default of 256m is too big for an early GC to kick in.


-Xscmx maximum size of shared classes cache
 The old default seems way too small.  And theJ9 defaults have grown significantly since Java 6.  For Java 8 (12.0.1) i set this to 300m, on newer versions I disable the setting to let the J9 engine decide.  A strategy also employed for other settings.  Reading the J9 documentation I got the impression that a LOT of effort is poured into making the JVM really smart, so I try to take advantage of that where I can.


-Xscmaxaot maximum size of AOT part of shared classes cache
 The AOT part refers to those classes compiled Ahead Of time (i.e. AOT).  This used to be set to 12m to not make the 64m shared class file too cramped.  But since that has grown a lot, I prefer to let J9 decide.


-Xminf0.1 minimum percentage of free space after global GC
 The 0.1 seems a size to stem from the resource constrained past.  These days, with plenty of memory, I think the better option is to let J9 do its thing.


-Xmn7m  size of the nursing or eden area of the heap
 Found a refernce to this value in an article descibing it being needed in a fix for Websphere Liberty on Java 8 on IBM DeveloperWorks.  From 2008...  I think that the necessity of setting it explicitly has expired a while ago.


-Xshareclasses  setup the shared classes cache  
Removed the singleJVM keyword to let the cache be shared.  Changed nonfatal to fatal so the Eclipse launcher throws an error if there is a problem with the shared classes file, instead of launching a really slow Notes client


-Xmaxt0.6  maximum proportion of time to spend in GC as a percentage of prevoius 3 GC runs
 If GC takes long, expand heap, if it is really wuick, shrink heap.  The J9 default is 0.13, the IBM/HCL default is 0.6.  Seemed like something to constrict heap growth based on the restricted availability of 32-bit heap space. I rather have Designer use some memory than doing tryhard heap compaction.   So I disable this to let J9 decide.


-Xgcpolicy:gencon which GC policy to use
 The default is gencon.  On 64-bit there is the option to use balanced, but I have not yet experimented with that.  In part because this opens a whole new can of worms to take into consideration.



Have fun tuning, and i'd love to hear from you!

RTF appending on the same document using LotusScript

Lars Berntrop-Bos  February 21 2024 02:42:10 PM
How to copy some Rich Text to from one field to another and not have to fool around with attachments
Simple, if you use this recipy.


Use case:

You have a form with a RichText field (Editable), the contents of which you want to add to another RichText field (computed), keeping the attachments. Excellent for keeping history.


Solution:

%REM
     Sub PostSave
     Prepend Comment to History Field
%END REM

Sub
PostSave(Source As NotesUIDocument)
     
Dim doc As NotesDocument
     
Dim rtReport As NotesRichTextItem
     
Dim rtReport As NotesRichTextItem
     
Dim rtTemp As NotesRichTextItem
     
Dim rtComment As NotesRichTextItem
     
Dim mustAddPart As Boolean
     
     
Set ws = New NotesUIWorkspace
     
Set doc = Source.Document
     
     
' get comment field, it's nam is convAddPart
     
Set rtComment = doc.GetFirstItem("convAddPart")
     
' Is there text in the Comment, or an attachment?
     mustAddPart =
Len(rtComment.Abstract(5, False, False)) > 0 Or Not IsEmpty(rtComment.EmbeddedObjects)
   
     
If mustAddPart Then
             
' Insert commentaar in start of history field, name is convReport
             
Set rtHistory = doc.GetFirstItem("convReport")
             wipeRT doc,
"convAddReportTemp"' make sure temp is empty
             
Set rtTemp = New NotesRichTextItem(doc, "convAddReportTemp") ' create temp RT
             rtTemp.AppendRTItem rtHistory
' save old report
             
Delete rtHistory ' kill object
             wipeRT doc,
"convReport" ' remove in doc, but leave $file
           
             
' now start building the report
             
Set rtHistory = New NotesRichTextItem(doc, "convReport") ' new item
             
' call helper routine that adds a bold header to rtHistory, and a line end
             
Call appendHeaderPlusTextToRT(rtHistory, Format$(Now, "dd/mm/yyyy hh:mm:ss") + ", " + ses.CommonUserName, "") ' user and timestamp in bold
             
' get Comment
             
Set rtComment = doc.GetFirstItem("convAddpart") ' get new part
             rtHistory.AppendRTItem rtComment
' append new part
             rtHistory.AddNewLine
2 ' blank lines
             rtHistory.AppendRTItem rtTemp
' append old report from rtTemp
             wipeRT doc,
"convAddReportTemp" ' wipe temp
             wipeRT doc,
"convAddPart" ' wipe comment
             rtHistory.Update
' process all updates
             rtHistory.Compact
' end of edits, compact
             
Call doc.ComputeWithForm(True, False)
             
Call doc.Save(True, False)
     
End If ' mustAddPart
     strDocid = doc.UniversalID   ;
' Get UNID
     mustAddPart = Source.EditMode
' get editmode
     
Call Source.Close
     ws.EditDocument mustAddPart, db.GetDocumentByUnid(strDocid), , ,
False
End Sub
' PostSave
%REM
     Sub wipeRT
     wipe RT field
%END REM

Sub Sub wipeRT(doc As NotesDocument, rtName As String)
     doc.ReplaceItemValue rtName,
"" ' OverWrite with a simple text field
     doc.RemoveItem rtName

End Sub
' PostSave

How does it work:

First, save the existing RichText to a temp RichText field.  rtTemp has a copy of the rtHistory, with references to any $File items holding the attachments.

Then I use wipeRT to replace the existing RT field with a simple text field, which leaves the $File items intact.

Then I use a helper routine to add a bold header with a timestamp and the user, and append the comment.  Any $File references are copied.

wipeRT is used to get rid of the temp field and empty the Comment field without losing the $File attachments.  If you call NotesRichTextItem.Remove directly, those $File items get destroyed, leaving a nice icon image in the RichText field, but no content.


Have fun!

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