« The New Battery Performs | Main| Did you know? »

01/29/2007

Dissecting the DDM Tabbed View Interface

Category   

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 Manager 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.

A picture named M2


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):

A picture named M3

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.

A picture named M4

The interesting thing about these tabs are that they 'change' their highlight based on the selected view.

A picture named M5

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.

A picture named M6

[[<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

Gravatar Image1 - Holy Cow, Andy...that's a nice bit of explaining there! Emoticon

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.

Gravatar Image2 - Wow, that's pretty freaking cool. I honestly stopped reading when I saw the "On Event..." line. Believe it or not, I've used this with the SAX Parser, but I never thought to apply it to LS classes. That makes things SOOO much easier!

Gravatar Image3 - Very clear explanation of what can be seen as a complex piece of code - certainly something that I'd not considered looking at. Nice work!

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

Gravatar Image4 - Andy,

*great* job of dissecting, analyzing and documenting this. You did a much better job than I did in our internal spec Emoticon . Thanks for writing this up!

Gravatar Image5 - Great postmortem on this template Andy! I'm in this database everyday, but had not thought much about the design and what ideas could be leveraged for other apps. Thanks for the great work!

Gravatar Image6 - Andy,

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

Gravatar Image7 - Is this an R7 template? I've looked in the advanced templates on one of our servers, but can't find this anywhere.

Gravatar Image8 - Yes it is an R7.x template/feature.

However, the concept can be easily applied to an R6 application...there isn't anything there that would keep it from working in R6.

Gravatar Image9 - Andy,

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.

Gravatar Image10 - quiet impressive description, too bad something went wrong with the layout which makes it difficut to read.

when is the follow up article coming? just curious to see if I can implement it easy in my apps Emoticon

Post A Comment

:-D:-o:-p:-x:-(:-):-\:angry::cool::cry::emb::grin::huh::laugh::lips::rolleyes:;-)

misc links

search my blog

domino blogger search

coComments

tag cloud