Skip to content
Home » Blog » Workstation-specific user application settings

Workstation-specific user application settings

For a couple of applications, I’ve needed a way to store values specific to the combination of a user and workstation. These are personal settings, in other words, but the same person may need different settings if they use the application on a different workstation.

This would mostly be local filepaths, things like the last folder the user selected for a particular file-open prompt, or the path of an external tool the user has to launch in a given situation. A list of recently accessed files. These would be different on different workstations.

Implementation strategy and alternatives

You could use environment variables for some of this, and that might be the answer in some situations. But there’s a limit to the amount of data you can conveniently store that way, and you have to worry about name conflicts, especially between different copies of the same application. Also, if it’s a shared workstation, you may want to allow for different values per user.

It makes sense to store these settings in the application itself. Profile documents are nice for this because they were designed for precisely that type of data access. They can be accessed by macro code as well as LotusScript. They’re aggressively cached for best performance, to the extent different LotusScript execution environments that have the same profile in memory, can see each others’ changes in real time. There’s only ever one copy in memory at a time in a given Notes process.

Personal profile documents are accessed by a key value and the username. The username is also an Authors item in the profile, allowing users with Author access to create and modify one. So the key value needs to be the part unique to the workstation.

Profiles and forms

To provide a unique key value per workstation, I assign each workstation a unique ID stored in an environment variable. Every application can use the same workstation ID INI setting — it’s fine if profiles in other applications have the same key.

The workstation ID, in my code, is stored in the environment variable LPD_ID. To allow use in macro code, it’s not a system variable. It’s generated with the @Unique function just once for the workstation, by whichever application first wants to use it and finds it missing. For LotusScript code, I’ll provide a script library to manage it. For macro language, it can be accessed as follows:

@GetProfileField(@Environment("LPD_ID"); "itemname"; @UserName)

“” (empty string) isn’t a valid value for the profile name argument. For formulas like this to work without error, they either have to check whether the ID has been assigned (which this one doesn’t do), or we have to make certain the ID is assigned before trying to evaluate the formula. This can be done in the Initialize event of the Database Script.

Book covers.

If you find this website helpful and want to give back, may I suggest buying, reading, and reviewing one of my excellent books? More are coming soon!

If you want email whenever there’s a new post, you can subscribe to the email list, which is 100% private and used only to send you information about stuff on this site.

Specification

The implementation is a LotusScript library, LocalProfileDoc, which declares and initializes a global object, GLocalProfile, of type LocalProfileDoc, with the following properties and methods:

  • Sub Init: intended to be called from the Initialize event of the Database Script, makes sure the workstation ID is defined in an INI setting.
  • WorkstationID (String, read-only): the ID of this workstation, generated via @Unique function then stored in an INI setting.
  • Profile (NotesDocument, read-only): the user/workstation profile document.
  • isDirty As Boolean (read/write): Tells whether any fields of the profile have been modified. If True, the profile is automatically saved on exit. If you use Profile property and change item values in the document, you must explicitly set this property.
  • Function GetValue(itemname) As Variant: Returns the value of a profile field as a scalar. If the field is multivalued, returns the first value.
  • Function GetValue(itemname) As Variant: Returns the value of a profile field as an array, similar to NotesDocument.GetItemValue method.
  • Sub SetValue(itemname, value): Assigns a profile field, if it doesn’t already have the specified value. The profile isn’t immediately saved. isDirty property shows whether the profile needs saving.
  • Sub Save: Save pending profile changes, if any. If isDirty is false, does nothing.
  • Function EditInDialog(formname$, title$, Autohorzfit As Boolean, Autovertfit As Boolean, Nocancel As Boolean, Nookcancel As Boolean, Okcancelatbottom As Boolean) As Boolean: Edit the profile document in a dialogbox using the specified form. If the user presses OK their changes are saved. Function returns True if changes are saved.

Source code

The implementation is a single script library, as follows:

%REM
	Library LocalProfileDoc
	
	This library allows for the creation of a Notes workstation-specific personal profile document.
	This can be used to store personal preferences limited to the current workstation. It's appropriate for such things as
	last-used filepaths for file open dialogs, paths of external tools -- anything involving a local filepath, or
	that can potentially be different on different workstations.
	
	To use this, have the database script contain the following code:
		Use "LocalProfileDoc"
		Sub Initialize
			GLocalProfile.Init ' make sure the workstation has an ID
		End Sub
		
	Where you need to use or set values in the local profile, use this library and the GLocalProfile object, or
	in macro language use a formula such as:
	
		@GetProfileField(@Environment("LPD_ID"); "itemname"; @UserName)
	
	© 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 Public
Option Declare

' Global used to access user/workstation profile document.
Public GLocalProfile As LocalProfileDoc
' INI setting containing unique workstation ID for user/workstation local profile docs.
Public Const LOCALPROFILE_ID_INI = "LPD_ID"

%REM
	Class LocalProfileDoc
	By Andre Guirard
	Description: Manages a profile document specific to this workstation+user combination.
	Constructor: Do not create, use existing global GLocalProfile.
%END REM
Class LocalProfileDoc
	Private z_workstationID As String
	Private z_db As NotesDatabase
	Private z_docPro As NotesDocument
	Public isDirty As Boolean
	
	%REM
		Sub Init
		Description: Make sure there's a workstation ID assigned.
	%END REM
	Function WorkstationID As String
		If z_workstationID = "" Then
			Dim ses As New NotesSession, tmp
			z_workstationID = ses.Getenvironmentstring(LOCALPROFILE_ID_INI, False)
			If z_workstationID = "" Then
				tmp = Evaluate(|@Right(@Unique; "-")|)
				z_workstationID = tmp(0)
				Call ses.Setenvironmentvar(LOCALPROFILE_ID_INI, z_workstationID, False)
			End If
		End If
		WorkstationID = z_workstationID
	End Function
	
	%REM
		Property Profile (read)
		Description: Get the profile document. If you change field values in the profile, you must set GLocalProfile.Dirty = True
			to make sure they're saved.
	%END REM
	Public Property Get Profile As NotesDocument
		If z_docPro Is Nothing Then
			Dim ses As New NotesSession
			If z_db Is Nothing Then
				Set z_db = ses.Currentdatabase
			End If
			Set z_docPro = z_db.Getprofiledocument(WorkstationID, ses.Username)
		End If
		Set Profile = z_docPro
	End Property
	
	%REM
		Function GetValue
		Description: Retrieve a single item value from the profile.
		Arguments:
			itemname: the name of the item to retrieve
		Returns:
			a SINGLE string, number, or date. If the item is multivalued use GetArray
	%END REM
	Function GetValue(ByVal itemname$) As Variant
		Dim tmp
		tmp = Profile.Getitemvalue(Itemname)
		If IsArray(tmp) Then GetValue = tmp(LBound(tmp)) Else GetValue = tmp
	End Function
	
	%REM
		Function GetArray
		Description: Retrieve a multivalued item value from the profile.
		Returns: an array even if it's a single item -- like NotesDocument.GetItemValue
	%END REM
	Function GetArray(ByVal itemname$) As Variant
		Dim tmp
		tmp = Profile.Getitemvalue(Itemname)
		If IsArray(tmp) Then GetArray = tmp Else GetArray = tmp
	End Function
	
	%REM
		Function fieldValsCompare
		Description: Compare two values read from document items to determine which is larger. Similar to StrComp.
			If one is an array and one is a scalar, the scalar is treated as a one-element array. The values
			may be strings, dates, or numbers, but they should be of the same type.
		Arguments:
			val1, val2: the values to compare -- may be arrays or scalars.
		Return value: -1 if val2 is "larger," 1 if val1 is larger, 0 if equal.
	%END REM
	Private Function fieldValsCompare(val1, val2, ByVal flags%) As Integer
		On Error GoTo oops
		fieldValsCompare = 1 ' if compare fails, always return 1.
		If IsArray(val1) Then
			If IsArray(val2) Then
				If UBound(val1) = UBound(val2) Then
					If DataType(val1(0)) = DataType(val2(0)) Then
						Dim i%, tmp%
						If DataType(val1(0)) = 8 Then
							For i = 0 To UBound(val1)
								tmp = StrComp(val1(i), val2(i), flags)
								If tmp <> 0 Then Exit For
							Next
						Else
							For i = 0 To UBound(val1)
								tmp = Sgn(val1(i)-val2(i))
								If tmp <> 0 Then Exit For
							Next
						End If
						fieldValsCompare = tmp
					End If
				End If
			End If
		Else
			If Not IsArray(val2) Then
				If DataType(val1) = DataType(val2) Then
					If DataType(val1) = 8 Then
						fieldValsCompare = StrCompare(val1, val2, flags)
					Else
						fieldValsCompare = Sgn(val1-val2)
					End If
				End If
			End If
		End If
oops:
		Exit Function ' returning not-equal
	End Function
	
	%REM
		Sub SetValue
		Description: Set the value of a profile item.
		Arguments:
	%END REM
	Sub SetValue(ByVal itemname$, value)
		Dim curval
		curval = Profile.Getitemvalue(Itemname)
		If fieldValsCompare(value, curval, 0) then
			Call Profile.Replaceitemvalue(Itemname, value)
			isDirty = True
		End If
	End Sub
	
	%REM
		Sub Save
		Description: Save any pending changes to profile document.
		Arguments:
	%END REM
	Sub Save
		If isDirty Then
			Call Profile.Save(True, False, True)
			isDirty = false
		End If
	End Sub
	
	%REM
		Sub Init
		Description: Make sure there's a workstation ID assigned to this workstation.
		Arguments:
	%END REM
	Sub Init
		Dim tmp$
		tmp = WorkstationID
	End Sub
	
	%REM
		Function EditInDialog
		Description: Edit the profile document in a dialog box. The changes are saved to the profile.
		Arguments:
			formname: What form to use in the dialog (since the profile key doesn't correspond to a form).
			other arguments as for NotesUIWorkspace.Dialogbox - some are omitted because e.g. nobody uses layout regions.
		Returns: True if user clicks OK.
	%END REM
	Function EditInDialog(ByVal formname$, ByVal title$, ByVal Autohorzfit As Boolean, ByVal Autovertfit As Boolean, _
		ByVal Nocancel As Boolean, ByVal Nookcancel As Boolean, ByVal Okcancelatbottom As Boolean) As Boolean
		Dim wksp As New NotesUIWorkspace
		Save ' any pending changes
		EditInDialog = wksp.Dialogbox(Formname, Autohorzfit, Autovertfit, Nocancel, false, false, false, Title, Profile, (Autohorzfit Or Autovertfit), Nookcancel, Okcancelatbottom)
		If EditInDialog Then
			isDirty = True
			Save
		End If
	End Function
	
	Sub Delete
		Save
	End Sub
End Class
Sub Initialize
	Set GLocalProfile = New LocalProfileDoc
End Sub

Final notes

There’s not a straightforward way to edit the profile in a full-screen window. You can, but it would require a specially designed form with special code on it to load and save the profile fields rather than allowing a normal document compose and save. Left as an exercise.

This tool’s value is enhanced if there’s also a library of UI code to make use of it. For instance, the Image Catalog application I’m working on for OpenNTF publication, contains a “file open dialog” UI function that takes a Situation argument, telling which file-open dialog it is so we can default to the last-used folder specific to that situation.

Leave a Reply

Your email address will not be published. Required fields are marked *