I’ve been working on a Notes client application recently where I ran into the problem that I needed to create a design element and then immediately use it. This ran afoul of the Notes client’s design element cache — it wouldn’t recognize the new design element until I closed the application and reopened it.
There’s no “close and reopen” command in any Notes scripting language, and if you close the current application completely, your code in that application stops running, so you can’t then execute a command to reopen it.
Here’s my solution. Someone will probably comment about some much simpler solution I’ve missed, such as a secret command to reload the design element cache, but this way works.
If you just want to use it…
Download the sample database.
Copy these design elements to your application:
- Form Pause Screen
- Script library DelayedAction
When you need to close and reopen the database, use this code:
Use "DelayedAction" ... Call DelayedAction(True, False, "Please wait", 2, "")
The arguments are:
- bClose: True if you want the subroutine to close all windows of the current application.
- bAllowExit: True if you want to let the end user carry out the reopen action early by closing the pause window.
- message is the text to display above the “processing” animation.
- delaySeconds is the delay time before carrying out the action.
- action can be either:
- “” or “opendb” to reopen the current application.
- “view:viewname” to reopen to a specified view or folder of the current application.
You can customize the Pause Screen form to accept other commands.
NOTE: when you test this out in your own application, the application needs to be closed in Domino Designer and Domino Administrator. If it’s open in any other client besides Notes, it won’t “really” be closed and the client’s cache about the application won’t be refreshed.
Overview of the implementation
To have code running to reopen the current database, that code needs to be in some other database. My plan was to create a “pause screen” that would use a NotesTimer to wait a specified number of seconds for the dust to settle, then execute some code it had been supplied — in this case, code to reopen the database that had recently been closed.
Since the database is closed during this process, this pause screen has to be a form or page in some other database. If you have some application with a customized design that you can rely on to be available at all times — including offline — you can put such logic there. However, I wanted a general-purpose solution that doesn’t make such assumptions.
So I chose to use bookmark.nsf, which is available on every client and to which the end user has guaranteed high level of access. But I didn’t want to add design elements to the bookmarks database, or to save anything, for that matter — I wanted a solution that would float lightly above the disk, saving nothing but just executing some code.
I decided this would require a stored form — a Notes document that has the form design and data in a single note. Since I don’t want to actually store the document, I build the stored form in memory. To figure out how to do that, I created the form I wanted in my application, ticked the option to store form in document, and actually saved it once. I then analyzed the items in the saved document versus the items in the form to see how to convert a form into a stored-form document via code. (I used Julian Robichaux’s wonderful Advanced Properties tool to do this comparison — I didn’t see how to get it to analyze a design element so I created a view of forms to be able to right-click it in the client (Julian tells me this is possible in the latest “pro” version of the tool). But I digress).
The code I ended up with is specific to that one form — possibly someone has written more general code to do this, but I searched the Interwebs with no luck.
Once you open it, this form executes code to start a timer running, then just sits there playing a “processing” animation until the timer runs out. At that point, it does the task specified by the caller, and closes itself.
I also wanted to allow for the end user to manually close the pause screen if they get tired of waiting — though for most purposes it should just be a couple seconds. It’s optional for the caller whether to allow this, and if they do close it, the code to do whatever still executes at that time.
The actual code
The pause screen has an animated GIF on it because I thought that would be fun. You have to import any graphics, not use an image resource, because the image resource will not be available in the bookmark.nsf database where the form is opened.
The form has “Store form in document” option ticked, has fields to receive the parameter values of the above function call, and contains this code:
(window title formula)
Message(Globals - Declarations)
Dim GTim As NotesTimer Dim UIDoc As NotesUIDocument Const NEWLINE = { } Const MSG_UNKNOWN_ACTION = "Unknown action: " Const MSG_ACTION_FAIL = |Failed attempting action "{0}"|(Form)
Sub Postopen(Source As Notesuidocument) Dim ses As New NotesSession, doc As NotesDocument Set GTim = ses.CreateTimer Set UIDoc = Source GTim.Interval = Source.Document.Interval(0) On Event alarm From GTim Call TimesUp GTim.Enabled = True End Sub Sub Queryclose(Source As Notesuidocument, Continue As Variant) If GTim.enabled Then ' they're trying to close while the event is still running If Source.Document.AllowExit(0) Then PerformAction Else Continue = False End If End If End Sub Sub TimesUp(Source As NotesTimer) Source.Enabled = False PerformAction UIDoc.Close End Sub Sub PerformAction On Error Goto oops Static done As Boolean If done Then Exit Sub Else done = True Dim wksp As New notesuiworkspace Dim action action = uidoc.Document.GetItemValue("Action") Select Case action(0) Case "opendb" wksp.OpenDatabase action(1), action(2) Case "openview" wksp.OpenDatabase action(1), action(2), action(3) Case Else Error 20000, MSG_UNKNOWN_ACTION & action(0) End Select Exit Sub oops: Msgbox Replace(MSG_ACTION_FAIL, "{0}", Join(action, ",")) & { } & Error Exit Sub End Sub
When the form opens, the Postopen event sets up a timer to run for the selected interval (supplied in the Interval field). When the timer finishes, the TimesUp function disables the timer, performs the function the caller requested, and closes its own window.
Note: a saved NotesUIDocument object is used here to close the current window, instead of NotesUIWorkspace.CurrentDocument, because the user isn’t locked to this window while the timer runs — some other document, or no document, might have focus at that time.
The code execution is handled by the PerformAction subroutine. A limited set of functions is supported — open a database or open a view in a database. You can of course add to them. An earlier version of this used the Execute statement to run whatever code the caller passed in, but this presented a security concern — it seemed likely an attacker could take advantage of this function to execute any code with the presumably higher ECL permissions of the signer of the application.
If the user tries to close the Pause window early, the Queryclose event checks the AllowExit field to determine whether this is allowed. If so, it calls PerformAction to execute the timer event function immediately, resulting in the timer action being performed early.
Here’s the script library code to close the calling application’s windows and open this form:
%REM Library DelayedAction Used in conjunction with the Pause Screen form, this library displays said form with text supplied by the caller, for a specified time, then it executes LotusScript code provided by the caller. The default action is to close and reopen the current database -- generally done to refresh the design element cache. © 2022 Andre Guirard Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. %END REM
Option Declare Private Const PLEASEWAIT = "Please hold..."'Begin DNT
Private Const FORMNAME = "PauseScreen" Private Const NEWLINE = { }' This will work by taking the above form and creating a shared-form document from it in memory, ' so we can then open that document. This requires copying some fields, renaming some in the process. ' Other fields are duplicated. This is not a general-purpose solution for this task, but it works for this form.
Private Const ITEMS_COPY = "$$Script_O" Private Const ITEMS_SF = |$Comment,$DesignerVersion,$Fields,$Script| Private Const ITEMS_BOTH = |$PublicAccess,$$ScriptName,$HTMLCode,$Info,$TITLE,$Fonts,$$FormScript,$$$FormScript_O,$WindowTitle|%REM Sub DelayedAction Optionally close the current application and after a specified delay, take some action (probably reopening it). Arguments: bClose: whether to close the current application. bAllowExit: whether to let the end user exit the Pause screen before the timer runs out (if they do, the action is carried out early). message: What message to display on the pause screen. If "" a default message is displayed. delaySeconds: how long to display the pause screen. action: What to do after the delay. Choices are: "" or "opendb" to open the current database. "view:" followed by a view name to open the current database to the specified view. or other command accepted by the Pause form, which you might customize for that purpose. %END REM
Public Sub DelayedAction(ByVal bClose As Boolean, ByVal bAllowExit As Boolean, ByVal message$, ByVal delaySeconds%, ByVal action$) Dim wksp As New NotesUIWorkspace, ses As New NotesSession Dim bookmark As New NotesDatabase("", "bookmark.nsf") Dim itCopy, itDup, itSF, itAction List As Integer' load the knowledge of how to convert a form to a stored-form document.
itCopy = Split(ITEMS_COPY, ",") itDup = Split(ITEMS_BOTH, ",") itSF = Split(ITEMS_SF, ",") ForAll aName In itCopy itAction(aName) = 1' just copy these items
End ForAll ForAll aName In itSF itAction(aName) = 2' rename to oldname_StoredForm
End ForAll ForAll aName In itDup itAction(aName) = 3' both copy the item and create a renamed copy.
End ForAll Dim docForm As NotesDocument, docSF As NotesDocument, nnc As NotesNoteCollection Dim dbThis As NotesDatabase Set dbThis = ses.Currentdatabase' locate the form that displays the pause screen.
Set nnc = dbThis.Createnotecollection(false) nnc.Selectforms = True nnc.Selectionformula = {$TITLE = "} & FORMNAME & {"} nnc.Buildcollection Set docForm = dbThis.Getdocumentbyid(nnc.Getfirstnoteid)' create the stored form document in the user's bookmarks database (it is not saved).
Set docSF = bookmark.Createdocument' copy items from the form to the stored form, renaming as needed.
ForAll item In docForm.Items Dim ac% If IsElement(Itaction(item.name)) Then ac = Itaction(item.name) If ac And 1 Then Call item.Copyitemtodocument(docSF, item.name) End If If ac And 2 Then Call item.Copyitemtodocument(docSF, item.name & "_StoredForm") End If End If End ForAll' some items are special. $Body needs to be copied at the end, after its supporting information.
Dim itum Set itum = docForm.Getfirstitem("$Body") Call itum.copyitemtodocument(docSF, "$Body_StoredForm") Call itum.copyitemtodocument(docSF, "$Body")' $Signature's copies have a different naming convention than other items.
Set itum = docForm.Getfirstitem("$Signature") Call itum.copyitemtodocument(docSF, "$SIG$Form") Call itum.copyitemtodocument(docSF, "$Signature_StoredForm") Call docSF.Sign' supply defaults for arguments.
If message = "" Then message = PLEASEWAIT If InStr(action, NEWLINE) Then' looks like they know what they're doing -- pass the command through.
ElseIf action = "" Or action = "opendb" Then action = "opendb" & NEWLINE & dbThis.Server & NEWLINE & dbThis.Filepath ElseIf action Like "view:*" Then action = "openview" & NEWLINE & dbThis.Server & NEWLINE & dbThis.Filepath & NEWLINE & Trim(StrRight(action, ":")) End If If bAllowExit Then docSF.AllowExit = 1 Else docSF.AllowExit = 0 docSF.Message = message docSF.Action = Split(action, NEWLINE) docSF.Interval = delaySeconds If bClose Then wksp.Currentdatabase.Close Call wksp.EditDocument(False, docSF, True) End Sub
When you combine a form and a document to create a stored form document, the resulting note has all the items that are stored in the document — e.g. message, action, …. It also contains copies of most of the items from the Form note, such as $Body — the rich-text body of the form, and $TITLE, the form name and alias. Some items are just copied over, some copies are renamed to include the suffix “_StoredForm“, and some items, for reasons I don’t understand, are copied twice — once with their original name and once with “_StoredForm” added. The signature on the form is copied two times, and given two different names, and the note is also signed with a second cryptographic signature which I guess includes the document items also (which I create using the NotesDocument.Sign method).
Implementation caveats
It’s a bit kludgey, but it works.
As mentioned, the code to create a stored form document in memory doesn’t necessarily work with all forms. If anyone has general code please mention it in the comments.
One of the two copies of the $Body item was a little longer than the original in my sample stored form document. Its CD records contained some added PAB definitions and PAB references. I don’t know the purpose of these extra records, and I didn’t include them in my copy because that would be too difficult — I just made two exact copies of the original $Body item. It didn’t affect anything in my case, but it might matter for other forms.
I’ve noticed it makes a difference which order you copy the $Body item (or any rich text item in a document), relative to its supporting items like $Links, $Fields, and $Fonts — you need to copy these other items first. That’s why the $Body field is handled as a separate case after the loop that copies all other items.