Dissecting the DDM Tabbed View Interface
Category Lotus Domino Lotus Notes Lotusscript
Love it or hate it, you must admit that the tabbed interface is generally pretty well understood by end users: clicking a tab takes you to a 'discrete' screen of information, just like a tabbed folder in a file drawer...used in moderation and with consistency, tabs can be your friend in helping your users organize their data.
Generally, in a Notes client environment, we see tabbed tables on forms or pages...this article addresses the tabbed interface used in association with a Notes Outline and multiple Notes Views (after the jump.)
If you haven't taken a look at the Domino DomainManager Monitoring (DDM) template because you are
a Notes/Domino application developer and not an admin, you are really doing
yourself a big disservice of not learning about a relatively unique approach
to building a high quality user interface inside of a Notes client application.
Most striking, in my opinion, is the DDM's use of a standard tabbed interface used at the view level. Interestingly, the view tabs follow your actions, even remembering your last opened view and highlighting the currently opened view reference.
We all have Thomas Gumz and Scott O'Keefe of Lotus/IBM to thank for this great example of fine UI programming in the Notes Client.
This article attempts to explain how the tabbed view interface works, in a future post I will explain how you can co-opt it for your own applications.
Diving deep into the DDM template will require looking at four sets of design elements: the application's whole frameset/frame architecture, a series of pages and the graphics on those pages, the Notes views that make up the application, and a script library ("lsDDMView") that contains the 'interesting' code that makes everything work.
So, without further ado...
Frameset/Frame Architecture
If you open the DDM application, the very first thing that happens is the $fsLaunch frameset is opened. It has one subframe, a computed frame without a name that encloses either the "$fsLaunch<190" frameset for older versions of Notes clients (this subframe contains a page with an error message); or "$fsLaunch>=190" a frameset that opens on clients greater than version 7 of Notes.
The "$fsLaunch>=190" frameset contains two subframes "frMainLeft" (a navigation frame) and "frMainRight" a content frame which contains the "$frMainInnerRight" frameset. In the "$frMainInnerRight" frameset there exists two subframes: a top frame holding a page for the tabbed elements called "pgEventsHeaderTabsRange" and a bottom frame holding a computed view for the requested categorization of documents called "frEvents". For now, we will just assume that the frame's computation evaluates to the default value of "vwEventsBySeverity1" (a view that holds the event documents that are of status 'Open' categorized by severity.
There is an important point to be made at this junction: view naming is critical to the operation of DDM. The number hanging at the end of the view's name (alias actually, a great best practice) must be a number (1-n) and the number describes which tab in the view's tabbed interface will be highlighted. The tabs' positions are order left to right starting with number 1 (thus "vwEventsBySeverity1".)
So our frameset/frame architecture looks like this (click to enlarge):
The frame structure is important because for this all to work correctly, DDM needs to be able to target specific frames for update, yet leave the underlying structure untouched.
Header Pages, Tabbed Graphics and the 'Main' & 'Top' Outlines
The frEventHeader frame opens a particularly composed page that contains the graphics that make up the 'tabs' that control the view choices.
The interesting thing about these tabs are that they 'change' their highlight based on the selected view.
This is because the images are all computed and contain Lotus' unique image caption.
The formula used to compute the images is as follows:
_mode := "1";
_active := "tab.active";
_inactive := "tab.inactive";
_range := @Environment("DDMViewRange");
@If(_range = _mode; _active; _inactive)
The _range variable reads an environment variable (stored in the Notes.INI file) called DDMViewRange. The DDMViewRange value is the same as the number suffix from the view name. In the first tab image above, the view name is "vwEventsBySeverity1" in the second tab image example the view name is "vwEventsBySeverity3". The "tab.active" is the highlighted tab image, the tab_inactive is the non-highlighted tab image. Those images combine with a precisely created page background image "TabFooterLine.gif" on top of a solid page background color (RGB 210,210,210) to create the desired effect. The "TabFooterLine.gif" is 43X34 pixels and is set to repeat in the horizontal mode only. The tab images are Lotus' unique 'two part' images that measure 261x28 pixels or 130x28 for each image mode with a 1 pixel gutter between the two images. I am not sure why
Lotus is using the two part image structure for these images as it doesn't
seem to be used in that manner on the page. The "tab.inactive"
has its first image as unhighlighted, and the second image as highlighted...this
allows for the image to change when you 'hover' over the inactive tabs.
The "tab.active" has two like images so when you hover
over it, it doesn't appear to change. Additionally, if you check
the DDM template's image resources, there are several other tab_active/tab_inactive
images available with unique 'icons'...I am not sure where these are used.
I should note that each of the images has an embedded computed hotspot that when clicked targets the frEvent frame and causes a different view to be loaded. The formula is as follows:
_viewSuffix := "1";
_frameName := "frEvents";
_viewBaseName := @Environment("DDMViewBaseName");
_viewName := _viewBaseName + _viewSuffix;
@SetTargetFrame(_frameName);
@Command([OpenView _viewName)
</code></blockquote>]]
The _viewSuffix value changes based on the particular tab's position to reflect which view gets loaded (in this instance we are looking at the first tab.) The _viewBaseName is pulled from a Notes.INI variable that holds the view name minus the suffix number (in this instance "vwEventsBySeverity".)
In the DDM template there are 3 views for each of the outline view selection options: By Severity, By Date, By Type, By Server, and By Assignment. This means that there are a total of 15 user accessible 'content' views. There is also a view called vwEventsMine that is treated specially, it is loaded by the topmost outline value that instead of loading a view into the frEvents frame, it loads a page with an embedded view that has a 'Show Single Category' formula that computes the Username for the value.
The Main outline is pretty simple. It has five main entries, one for each 'type' of view that computes a specific named element (a view) and targets the frEvents frame. The formulas in the 'named element' computation looks like this:
[[<blockquote><code>
_range := @Environment("DDMViewRange");
_viewBaseName := "vwEventsBySeverity";
_viewRange := @If(_range="";"1";_range);
_viewBaseName + _viewRange
</code></blockquote>]]
The only thing that changes is the _viewBaseName which would reflect the 'type' of view we want. Also note the _range value...it looks out at the Notes.INI to find its value...if there isn't a value available it uses the default of '1'. This allows the outline to 'learn' which view of this 'type' you last had loaded.
Views
All of the user accessible content views are pretty much standard Notes views with one major exception. In each view's Global Options a LotusScript library ("lsClassDDMView") is loaded with a Use statement. Then in the view's Global Declarations the following code is placed:
[[<blockquote><code>
Public Const MODULE_NAME = "vwEventsBySeverity1"
Dim oDDMView As cDDMView
</code></blockquote>]]
The MODULE_NAME constant varies by whichever view the code is attached to, but the variable declaration for oDDMView stays the same. The declaration is allocating space for an object variable built from the cDDMView class which is defined in the 'lsClassDDMView' library (more on this later.)
Finally, in the view's QueryOpen event, you will find the code:
[[<blockquote><code>
On Error Goto ERROR_HANDLER
Set oDDMView = New cDDMView(source)
Exit Sub
ERROR_HANDLER:
Call oException.HandleError(MODULE_NAME, "", Null)
Exit Sub
End Sub
</code></blockquote>]]
which initializes the oDDMView object with some error trapping/handling. The 'source' value that is being passed to the oDDMView constructor is the NotesUIView object representing the view that has been requested to open.
All of this code is consistent for any view that is normally accessible by the user.
lsClassDDMView Script Library
The most interesting piece of the whole DDM template puzzle is the 'lsClassDDMView' script library. It contains, as we have already seen, the class definition for the cDDMView. Using classes in LotusScript is beginning to be more popular (Bill Buchan had a great session on OO usage in Lotusscript at Lotusphere2007), but this class goes one step further by binding the object to the view's various events...in particular to the PostOpen event. The object code that gets executed when the view triggers the PostOpen event makes the DDM template do its magic and is an extremely valuable technique to learn.
Before I describe the contents of the lsClassDDMView library, I should note that it loads the 'lsCommon' library as well which contains a variety of 'utility' functions and public constants...when they are used, I will make notes.
cDDMView Class
[[<blockquote><code>
Public Class cDDMView
Private m_oSession As NotesSession
Private m_oView As NotesView
Private m_liViewHelp List As String
</code></blockquote>]]
This code sets up the cDDMView class and establishes three private member variables, these variables get initialized during the object construction process and I will go into further detail at that point.
When you instantiate a new object from a class definition, the New subroutine is what is used to 'construct' the object.
Here is the code for the cDDMView class' constructor
Sub New
[[<blockquote><code>
Public Sub New(Source As NotesUIView) 'class constructor
On Error Goto ERROR_HANDLER
Set m_oSession = New NotesSession
If Source Is Nothing Then
Error 5000, sprintf1(ERR_NOTESUIVIEW_REQUIRED, CLASS_NAME)
End If
Call Me.Initialize
'bind UI view events
On Event PostOpen From Source Call EventPostOpen
Exit Sub
ERROR_HANDLER:
Call oException.RaiseError(MODULE_NAME, CLASS_NAME, Null)
Exit Sub
End Sub
</code></blockquote>]]
This is a pretty simple piece of code. It has error handling (yea!!!) and sets the m_oSession member to the NotesSession. It then checks to make sure that the source (the passed NotesUIVIew) is an object, and then calls the Initialize subroutine. The Initialize routine simply assigns various Constant strings to the m_liViewHelp list member...these strings are used for errorhandling and Notes.INI variable setting. After running the Initialize routine, the object is bound to the source NotesUIView's PostOpen event and when that event is triggered, the object will run its own 'EventPostOpen' routine. This is the most critical piece to all of this code! The rest of 'New' is error handling, notable in the error handling is the use of sprintf1 a utility routine called from lsCommon and oException, a utility class object also defined in lsCommon and instantiated in lsCommon's Initialize routine.
Sub EventPostOpen
[[<blockquote><code>
Private Sub EventPostOpen(Source As NotesUIView)
On Error Goto ERROR_HANDLER
Dim oWorkspace As New NotesUIWorkspace
Dim sViewAlias As String
Dim sViewBaseName As String
Dim sViewRange As String
Dim sViewHelp As String
Dim bIsCustomView As Boolean
</code></blockquote>]]
This is pretty self explanatory...the routine needs some variable to work with, I learned last week at Lotusphere2007, that the NoteSession and NotesUIWorkspace, regardless of how, where and when instantiated, will only exist once in memory (known as a singleton in OOP)
[[<blockquote><code>
Set m_oView = Source.View
If Not Isempty(m_oView.Aliases) Then
sViewAlias = m_oView.Aliases(0)
Else
sViewAlias = m_oView.Name
End If
</code></blockquote>]]
This code sets the member object m_oView equal to the front-end source (NotesUIView) back-end NotesView and then assigns the sViewAlias string variable the name of the view, starting with the first alias name for the view.
[[<blockquote><code>
'(weak) attempt to try to determine whether the view is a custom view or not
If Left$(sViewAlias, Len(VIEW_ALIAS_PREFIX)) <> VIEW_ALIAS_PREFIX Then
bIsCustomView = True
Else
bIsCustomView = False
End If
</code></blockquote>]]
The code has the ability to handle custom views and checks to see if the view is a custom view or not...this is not a very accurate check by the way (so don't depend on it working.)
[[<blockquote><code>
'make sure our main frameset is open (if launched thru viewlinks, etc.)
Call oWorkspace.SetTargetFrame("")
Call oWorkspace.OpenFrameSet(FRAMESET_MAIN)
</code></blockquote>]]
This code sets the target frame to the topmost frame and reloads the $lsLaunch frameset
[[<blockquote><code>
'invalidate the top 'My events' outline by reloading it
If sViewAlias <> VIEW_ALIAS_MINE Then
Call oWorkspace.SetTargetFrame(FRAME_OUTLINE_TOP)
Call oWorkspace.OpenPage(PAGE_OUTLINE_TOP)
End If
</code></blockquote>]]
This code makes sure the outlines are reloaded properly (the My Events views, with their unique load requirements (a page with an embedded view instead of just a view) cause a problem with the outline highlighting
[[<blockquote><code>
'persist the current view name to notes.ini
Call m_oSession.SetEnvironmentVar(INI_VIEW_LAST_USED, sViewAlias, False)
</code></blockquote>]]
This writes the current view name to the Notes.INI so that DDM knows which view was last used so it can open that view the next time the app is run.
[[<blockquote><code>
'persist the current view help text to notes.ini (the tab page picks it up from there)
If Iselement(m_liViewHelp(sViewAlias)) Then
sViewHelp = m_liViewHelp(sViewAlias)
sViewHelp = sprintf1(sViewHelp, m_oSession.CommonUserName)
Call m_oSession.SetEnvironmentVar(INI_VIEW_HELP, sViewHelp, False)
End If
</code></blockquote>]]
This writes a string to the Notes.INI so that a 'view comment' can be calculated by the page that holds the tab graphics and displayed on the page as well.
[[<blockquote><code>
'handling of the view 'tabs'
Select Case sViewAlias
Case VIEW_ALIAS_MINE
'show special tabs page for this view
Call oWorkspace.SetTargetFrame(FRAME_TABS)
Call oWorkspace.OpenPage(PAGE_TABS_MINE)
Case VIEW_ALIAS_CORR
'don't change the view base name or range
Exit Sub
Case Else
If bIsCustomView = False Then
'show the range-tabs for all other views
'persist the current view range (the number part of the view name) to notes.ini
sViewRange = Right(sViewAlias, 1)
Call m_oSession.SetEnvironmentVar(INI_VIEW_RANGE, sViewRange, False)
'persist the current view base name (without the number) to notes.ini
sViewBaseName = Left(sViewAlias, Len(sViewAlias) - 1)
Call m_oSession.SetEnvironmentVar(INI_VIEW_BASE_NAME, sViewBaseName, False)
'reload the events header page to refresh computed tab images
Call oWorkspace.SetTargetFrame(FRAME_TABS)
Call oWorkspace.OpenPage(PAGE_TABS_RANGE)
Else
'show special tabs page for this view
Call oWorkspace.SetTargetFrame(FRAME_TABS)
Call oWorkspace.OpenPage(PAGE_TABS_CUSTOM)
End If
End Select
</code></blockquote>]]
This select statement does the heavy lifting...if the view is the 'My Events' view (Case VIEW_ALIAS_MINE), it targets the whole $fsMainRight frameset and loads the 'pgMyEvents' page (with its embedded single category view.) If the view is the same (Case VIEW_ALIAS_CORR) then it exits the sub and does nothing.
If the view is not one of those two cases, it first checks to see if it is a custom view. If the view is a standard view then pulls the view's suffix number from the view name Right(sViewAlias, 1)) and it trims the view name to not include the suffix number Left(sViewAlias, Len(sViewAlias) - 1) then it writes these values out to the Notes.INI. Then it targets the "frEventsHeader" frame and loads "pgEventsHeaderTabsRange" page into that frame. The computed image placeholders on that page then calculate the appropriate graphic to display (the corresponding tab to the suffix number gets the 'highlighted' graphic and the other tabs get the 'inactive' graphic.)
Finally, if the view is a custom view, the app targets the "frEventsHeader" frame and loads the "pgEventsHeaderTabsCustom" page which has a single tab graphic that says 'Custom View'.
[[<blockquote><code>
Exit Sub
ERROR_HANDLER:
Call oException.HandleError(MODULE_NAME, CLASS_NAME, Null)
Exit Sub
End Sub
</code></blockquote>]]
And the rest is error handling (again using the oException object from the lsCommon library.)
The class has two additional subroutines that are currently unused stubs for the future: "EventQueryOpenDocument" (obviously to be bound to the QueryOpenDocument event) and "QueryOpenDocument" which must be a placeholder for an internal event in the class rather than bound to the source NotesUIView.
Conclusion
In my next blog article on this topic, I will explain how I have co-opted this technique to use in my own applications...it is very easy to modify and use on your own...as experienced coders say "It is all R&D (Rob and Duplicate)"
Love it or hate it, you must admit that the tabbed interface is generally pretty well understood by end users: clicking a tab takes you to a 'discrete' screen of information, just like a tabbed folder in a file drawer...used in moderation and with consistency, tabs can be your friend in helping your users organize their data.
Generally, in a Notes client environment, we see tabbed tables on forms or pages...this article addresses the tabbed interface used in association with a Notes Outline and multiple Notes Views (after the jump.)
If you haven't taken a look at the Domino Domain
Most striking, in my opinion, is the DDM's use of a standard tabbed interface used at the view level. Interestingly, the view tabs follow your actions, even remembering your last opened view and highlighting the currently opened view reference.
We all have Thomas Gumz and Scott O'Keefe of Lotus/IBM to thank for this great example of fine UI programming in the Notes Client.
This article attempts to explain how the tabbed view interface works, in a future post I will explain how you can co-opt it for your own applications.
Diving deep into the DDM template will require looking at four sets of design elements: the application's whole frameset/frame architecture, a series of pages and the graphics on those pages, the Notes views that make up the application, and a script library ("lsDDMView") that contains the 'interesting' code that makes everything work.
So, without further ado...
Frameset/Frame Architecture
If you open the DDM application, the very first thing that happens is the $fsLaunch frameset is opened. It has one subframe, a computed frame without a name that encloses either the "$fsLaunch<190" frameset for older versions of Notes clients (this subframe contains a page with an error message); or "$fsLaunch>=190" a frameset that opens on clients greater than version 7 of Notes.
The "$fsLaunch>=190" frameset contains two subframes "frMainLeft" (a navigation frame) and "frMainRight" a content frame which contains the "$frMainInnerRight" frameset. In the "$frMainInnerRight" frameset there exists two subframes: a top frame holding a page for the tabbed elements called "pgEventsHeaderTabsRange" and a bottom frame holding a computed view for the requested categorization of documents called "frEvents". For now, we will just assume that the frame's computation evaluates to the default value of "vwEventsBySeverity1" (a view that holds the event documents that are of status 'Open' categorized by severity.
There is an important point to be made at this junction: view naming is critical to the operation of DDM. The number hanging at the end of the view's name (alias actually, a great best practice) must be a number (1-n) and the number describes which tab in the view's tabbed interface will be highlighted. The tabs' positions are order left to right starting with number 1 (thus "vwEventsBySeverity1".)
So our frameset/frame architecture looks like this (click to enlarge):
The frame structure is important because for this all to work correctly, DDM needs to be able to target specific frames for update, yet leave the underlying structure untouched.
Header Pages, Tabbed Graphics and the 'Main' & 'Top' Outlines
The frEventHeader frame opens a particularly composed page that contains the graphics that make up the 'tabs' that control the view choices.
The interesting thing about these tabs are that they 'change' their highlight based on the selected view.
This is because the images are all computed and contain Lotus' unique image caption.
The formula used to compute the images is as follows:
_mode := "1";
_active := "tab.active";
_inactive := "tab.inactive";
_range := @Environment("DDMViewRange");
@If(_range = _mode; _active; _inactive)
The _range variable reads an environment variable (stored in the Notes.INI file) called DDMViewRange. The DDMViewRange value is the same as the number suffix from the view name. In the first tab image above, the view name is "vwEventsBySeverity1" in the second tab image example the view name is "vwEventsBySeverity3". The "tab.active" is the highlighted tab image, the tab_inactive is the non-highlighted tab image. Those images combine with a precisely created page background image "TabFooterLine.gif" on top of a solid page background color (RGB 210,210,210) to create the desired effect. The "TabFooterLine.gif" is 43X34 pixels and is set to repeat in the horizontal mode only. The tab images are Lotus' unique 'two part' images that measure 261x28 pixels or 130x28 for each image mode with a 1 pixel gutter between the two images.
I should note that each of the images has an embedded computed hotspot that when clicked targets the frEvent frame and causes a different view to be loaded. The formula is as follows:
_viewSuffix := "1";
_frameName := "frEvents";
_viewBaseName := @Environment("DDMViewBaseName");
_viewName := _viewBaseName + _viewSuffix;
@SetTargetFrame(_frameName);
@Command([OpenView _viewName)
</code></blockquote>]]
The _viewSuffix value changes based on the particular tab's position to reflect which view gets loaded (in this instance we are looking at the first tab.) The _viewBaseName is pulled from a Notes.INI variable that holds the view name minus the suffix number (in this instance "vwEventsBySeverity".)
In the DDM template there are 3 views for each of the outline view selection options: By Severity, By Date, By Type, By Server, and By Assignment. This means that there are a total of 15 user accessible 'content' views. There is also a view called vwEventsMine that is treated specially, it is loaded by the topmost outline value that instead of loading a view into the frEvents frame, it loads a page with an embedded view that has a 'Show Single Category' formula that computes the Username for the value.
The Main outline is pretty simple. It has five main entries, one for each 'type' of view that computes a specific named element (a view) and targets the frEvents frame. The formulas in the 'named element' computation looks like this:
[[<blockquote><code>
_range := @Environment("DDMViewRange");
_viewBaseName := "vwEventsBySeverity";
_viewRange := @If(_range="";"1";_range);
_viewBaseName + _viewRange
</code></blockquote>]]
The only thing that changes is the _viewBaseName which would reflect the 'type' of view we want. Also note the _range value...it looks out at the Notes.INI to find its value...if there isn't a value available it uses the default of '1'. This allows the outline to 'learn' which view of this 'type' you last had loaded.
Views
All of the user accessible content views are pretty much standard Notes views with one major exception. In each view's Global Options a LotusScript library ("lsClassDDMView") is loaded with a Use statement. Then in the view's Global Declarations the following code is placed:
[[<blockquote><code>
Public Const MODULE_NAME = "vwEventsBySeverity1"
Dim oDDMView As cDDMView
</code></blockquote>]]
The MODULE_NAME constant varies by whichever view the code is attached to, but the variable declaration for oDDMView stays the same. The declaration is allocating space for an object variable built from the cDDMView class which is defined in the 'lsClassDDMView' library (more on this later.)
Finally, in the view's QueryOpen event, you will find the code:
[[<blockquote><code>
On Error Goto ERROR_HANDLER
Set oDDMView = New cDDMView(source)
Exit Sub
ERROR_HANDLER:
Call oException.HandleError(MODULE_NAME, "", Null)
Exit Sub
End Sub
</code></blockquote>]]
which initializes the oDDMView object with some error trapping/handling. The 'source' value that is being passed to the oDDMView constructor is the NotesUIView object representing the view that has been requested to open.
All of this code is consistent for any view that is normally accessible by the user.
lsClassDDMView Script Library
The most interesting piece of the whole DDM template puzzle is the 'lsClassDDMView' script library. It contains, as we have already seen, the class definition for the cDDMView. Using classes in LotusScript is beginning to be more popular (Bill Buchan had a great session on OO usage in Lotusscript at Lotusphere2007), but this class goes one step further by binding the object to the view's various events...in particular to the PostOpen event. The object code that gets executed when the view triggers the PostOpen event makes the DDM template do its magic and is an extremely valuable technique to learn.
Before I describe the contents of the lsClassDDMView library, I should note that it loads the 'lsCommon' library as well which contains a variety of 'utility' functions and public constants...when they are used, I will make notes.
cDDMView Class
[[<blockquote><code>
Public Class cDDMView
Private m_oSession As NotesSession
Private m_oView As NotesView
Private m_liViewHelp List As String
</code></blockquote>]]
This code sets up the cDDMView class and establishes three private member variables, these variables get initialized during the object construction process and I will go into further detail at that point.
When you instantiate a new object from a class definition, the New subroutine is what is used to 'construct' the object.
Here is the code for the cDDMView class' constructor
Sub New
[[<blockquote><code>
Public Sub New(Source As NotesUIView) 'class constructor
On Error Goto ERROR_HANDLER
Set m_oSession = New NotesSession
If Source Is Nothing Then
Error 5000, sprintf1(ERR_NOTESUIVIEW_REQUIRED, CLASS_NAME)
End If
Call Me.Initialize
'bind UI view events
On Event PostOpen From Source Call EventPostOpen
Exit Sub
ERROR_HANDLER:
Call oException.RaiseError(MODULE_NAME, CLASS_NAME, Null)
Exit Sub
End Sub
</code></blockquote>]]
This is a pretty simple piece of code. It has error handling (yea!!!) and sets the m_oSession member to the NotesSession. It then checks to make sure that the source (the passed NotesUIVIew) is an object, and then calls the Initialize subroutine. The Initialize routine simply assigns various Constant strings to the m_liViewHelp list member...these strings are used for errorhandling and Notes.INI variable setting. After running the Initialize routine, the object is bound to the source NotesUIView's PostOpen event and when that event is triggered, the object will run its own 'EventPostOpen' routine. This is the most critical piece to all of this code! The rest of 'New' is error handling, notable in the error handling is the use of sprintf1 a utility routine called from lsCommon and oException, a utility class object also defined in lsCommon and instantiated in lsCommon's Initialize routine.
Sub EventPostOpen
[[<blockquote><code>
Private Sub EventPostOpen(Source As NotesUIView)
On Error Goto ERROR_HANDLER
Dim oWorkspace As New NotesUIWorkspace
Dim sViewAlias As String
Dim sViewBaseName As String
Dim sViewRange As String
Dim sViewHelp As String
Dim bIsCustomView As Boolean
</code></blockquote>]]
This is pretty self explanatory...the routine needs some variable to work with, I learned last week at Lotusphere2007, that the NoteSession and NotesUIWorkspace, regardless of how, where and when instantiated, will only exist once in memory (known as a singleton in OOP)
[[<blockquote><code>
Set m_oView = Source.View
If Not Isempty(m_oView.Aliases) Then
sViewAlias = m_oView.Aliases(0)
Else
sViewAlias = m_oView.Name
End If
</code></blockquote>]]
This code sets the member object m_oView equal to the front-end source (NotesUIView) back-end NotesView and then assigns the sViewAlias string variable the name of the view, starting with the first alias name for the view.
[[<blockquote><code>
'(weak) attempt to try to determine whether the view is a custom view or not
If Left$(sViewAlias, Len(VIEW_ALIAS_PREFIX)) <> VIEW_ALIAS_PREFIX Then
bIsCustomView = True
Else
bIsCustomView = False
End If
</code></blockquote>]]
The code has the ability to handle custom views and checks to see if the view is a custom view or not...this is not a very accurate check by the way (so don't depend on it working.)
[[<blockquote><code>
'make sure our main frameset is open (if launched thru viewlinks, etc.)
Call oWorkspace.SetTargetFrame("")
Call oWorkspace.OpenFrameSet(FRAMESET_MAIN)
</code></blockquote>]]
This code sets the target frame to the topmost frame and reloads the $lsLaunch frameset
[[<blockquote><code>
'invalidate the top 'My events' outline by reloading it
If sViewAlias <> VIEW_ALIAS_MINE Then
Call oWorkspace.SetTargetFrame(FRAME_OUTLINE_TOP)
Call oWorkspace.OpenPage(PAGE_OUTLINE_TOP)
End If
</code></blockquote>]]
This code makes sure the outlines are reloaded properly (the My Events views, with their unique load requirements (a page with an embedded view instead of just a view) cause a problem with the outline highlighting
[[<blockquote><code>
'persist the current view name to notes.ini
Call m_oSession.SetEnvironmentVar(INI_VIEW_LAST_USED, sViewAlias, False)
</code></blockquote>]]
This writes the current view name to the Notes.INI so that DDM knows which view was last used so it can open that view the next time the app is run.
[[<blockquote><code>
'persist the current view help text to notes.ini (the tab page picks it up from there)
If Iselement(m_liViewHelp(sViewAlias)) Then
sViewHelp = m_liViewHelp(sViewAlias)
sViewHelp = sprintf1(sViewHelp, m_oSession.CommonUserName)
Call m_oSession.SetEnvironmentVar(INI_VIEW_HELP, sViewHelp, False)
End If
</code></blockquote>]]
This writes a string to the Notes.INI so that a 'view comment' can be calculated by the page that holds the tab graphics and displayed on the page as well.
[[<blockquote><code>
'handling of the view 'tabs'
Select Case sViewAlias
Case VIEW_ALIAS_MINE
'show special tabs page for this view
Call oWorkspace.SetTargetFrame(FRAME_TABS)
Call oWorkspace.OpenPage(PAGE_TABS_MINE)
Case VIEW_ALIAS_CORR
'don't change the view base name or range
Exit Sub
Case Else
If bIsCustomView = False Then
'show the range-tabs for all other views
'persist the current view range (the number part of the view name) to notes.ini
sViewRange = Right(sViewAlias, 1)
Call m_oSession.SetEnvironmentVar(INI_VIEW_RANGE, sViewRange, False)
'persist the current view base name (without the number) to notes.ini
sViewBaseName = Left(sViewAlias, Len(sViewAlias) - 1)
Call m_oSession.SetEnvironmentVar(INI_VIEW_BASE_NAME, sViewBaseName, False)
'reload the events header page to refresh computed tab images
Call oWorkspace.SetTargetFrame(FRAME_TABS)
Call oWorkspace.OpenPage(PAGE_TABS_RANGE)
Else
'show special tabs page for this view
Call oWorkspace.SetTargetFrame(FRAME_TABS)
Call oWorkspace.OpenPage(PAGE_TABS_CUSTOM)
End If
End Select
</code></blockquote>]]
This select statement does the heavy lifting...if the view is the 'My Events' view (Case VIEW_ALIAS_MINE), it targets the whole $fsMainRight frameset and loads the 'pgMyEvents' page (with its embedded single category view.) If the view is the same (Case VIEW_ALIAS_CORR) then it exits the sub and does nothing.
If the view is not one of those two cases, it first checks to see if it is a custom view. If the view is a standard view then pulls the view's suffix number from the view name Right(sViewAlias, 1)) and it trims the view name to not include the suffix number Left(sViewAlias, Len(sViewAlias) - 1) then it writes these values out to the Notes.INI. Then it targets the "frEventsHeader" frame and loads "pgEventsHeaderTabsRange" page into that frame. The computed image placeholders on that page then calculate the appropriate graphic to display (the corresponding tab to the suffix number gets the 'highlighted' graphic and the other tabs get the 'inactive' graphic.)
Finally, if the view is a custom view, the app targets the "frEventsHeader" frame and loads the "pgEventsHeaderTabsCustom" page which has a single tab graphic that says 'Custom View'.
[[<blockquote><code>
Exit Sub
ERROR_HANDLER:
Call oException.HandleError(MODULE_NAME, CLASS_NAME, Null)
Exit Sub
End Sub
</code></blockquote>]]
And the rest is error handling (again using the oException object from the lsCommon library.)
The class has two additional subroutines that are currently unused stubs for the future: "EventQueryOpenDocument" (obviously to be bound to the QueryOpenDocument event) and "QueryOpenDocument" which must be a placeholder for an internal event in the class rather than bound to the source NotesUIView.
Conclusion
In my next blog article on this topic, I will explain how I have co-opted this technique to use in my own applications...it is very easy to modify and use on your own...as experienced coders say "It is all R&D (Rob and Duplicate)"






Comments
I don't have time to digest all of this now, but thanks for pointing this out. I'm looking forward to reading this (and your follow up article) in more detail.
Posted by Chris Blatnick at 04:31:38 PM on 01/30/2007 | - Website - |
Posted by Charles Robinson at 05:22:00 PM on 01/30/2007 | - Website - |
I usually work on the back end and tend not to bind objects to UI events.. wow.. Nice one. I can see how this might help us..
Cheers, and keep up the good work!
---* Bill
Posted by Wild Bill at 08:35:56 PM on 01/30/2007 | - Website - |
*great* job of dissecting, analyzing and documenting this. You did a much better job than I did in our internal spec
Posted by Thomas Gumz at 09:24:59 PM on 01/30/2007 | - Website - |
Posted by Darren Johnston at 06:51:30 PM on 01/31/2007 | - Website - |
Your blog has been Taking Notes dotted....at least it will be on Friday when Episode 53 of the podcast comes out. We had the honor of interviewing Thomas Gumz where we talked about this blog post!
Bruce
Posted by Bruce Elgort at 01:20:13 AM on 02/08/2007 | - Website - |
Posted by Esther Strom at 03:13:30 PM on 02/08/2007 | - Website - |
However, the concept can be easily applied to an R6 application...there isn't anything there that would keep it from working in R6.
Posted by Andy Broyles at 03:32:54 PM on 02/08/2007 | - Website - |
I just found out about your blog and would like to comment that the technique that is used here is similar to the technique that is used in the Workplace welcome page. If you dissect the code, you will find that you can also use a set of tables and cells to have the same effect as the tab images. This technique was used starting in Notes 6.
Posted by Richard Moy at 11:05:24 PM on 02/13/2007 | - Website - |
when is the follow up article coming? just curious to see if I can implement it easy in my apps
Posted by Patrick Kwinten at 06:13:05 AM on 01/22/2008 | - Website - |