ShowTable of Contents
This article demonstrates how you can register listeners written in JavaScript language to get notified by Eclipse about the activation of viewparts and perspectives by the user.
A viewpart is an area of the Notes/Eclipse UI that displays content, like an XPages application, a classic Notes view or a component in the Client's sidebar.
In the Notes Client, Eclipse perspectives are represented as single tabs in the main tab bar at the top of the window.
There are three main use cases for tracking the user's navigation in the Client UI:
- provide context based information in XPages sidebar components
- write code that processes the current selection (e.g. Notes documents in a view)
- log visited databases, documents and views, for example for bookmarking or time recording purposes
Up until now, writing an
IPartListener or an
IPerspectiveListener for Eclipse required Java knowledge and had to be done using an Eclipse plugin. There was no easy way to read the current selection in an XPages application in the sidebar.
XPages2Eclipse simplifies this process and makes the functionality accessible for XPages developers in JavaScript.
Please note:
It is recommended that you read the article about the NSF classloader first before you go on reading this article.Sample application
Our sample application contains one XPage with two text fields that display the incoming perspective and part activation events. A third text field is used for an alternative approach to track part activation, which consumes less resources and is easier to use than writing your own listener.
There are buttons in the XPage to register and unregister the JavaScript listeners. After listener registration, you will notice that a text "new updates" will appear when you move the focus e.g. between the sidebar and the application tab or switch between different main tabs.
The message will disappear when you click on the "refresh" link to invoke a partial refresh on the corresponding text field and display the new event information.
This sample demonstrates the following techniques:
- listener registration
- data exchange between XPages application and the listener code
- listener code executing JavaScript on an XPages viewpart
- and using the viewpart activation history feature as an alternative to writing your own listener
The code to register the perspective listener uses a Java Hashtable
to pass data to the invoked listener code. The same dataMap
object is passed to the invoked code on any listener event call and can be used to add some context information to the whole process, in our case we transfer the primary and secondary ID of the current viewpart (the XPages application) to the listener as well as a Java Vector
that the listener code will use to report details about the event back to the application.
The primary/secondary viewpart IDs will be used in our listener code later to locate the XPages application and make it display the "new updates" message.
var conn=X2E.createConnection();
var pUI=com.x2e.PlatformUIAPI.getUI(conn);
var wb=pUI.getWorkbench();
var activeWindow=wb.getActiveWorkbenchWindow();
var partService=activeWindow.getPartService();
//pass a Map to the listener, used for data exchange
var dataMap=new java.util.Hashtable();
sessionScope.perspectiveListenerContent=dataMap;
//read information about current viewpart and store it
//in the dataMap that will be passed to the listener code
var activePart=partService.getActivePart();
if (activePart) {
dataMap.originatorPrimaryId=activePart.getSite().getId();
if (activePart instanceof com.mindoo.remote.api.org.eclipse.ui.IRemoteViewPart) {
dataMap.originatorSecondaryId=activePart.getViewSite().getSecondaryId();
}
}
//unique id, can be used to remove listener
var uniqueId="x2esamples-listeners";
//location of SSJS library
var dbPath=database.getFilePath();
var dbServer=database.getServer();
var libraryName="Listeners";
var methodName="perspectiveListener";
//only display perspective activation (use -1 to show all events)
var enableBitMask=com.mindoo.remote.api.org.eclipse.ui.IRemotePageService.PERSPECTIVEACTIVATED;
var resultList=new java.util.Vector();
dataMap.put("result", resultList);
activeWindow.addPerspectiveListenerSSJSLib(enableBitMask, uniqueId, dbServer, dbPath,
libraryName, methodName, dataMap);
For the listener registration, we pass a bitmask of interesting event types (see the
Javadocs for details) or simply -1 to get called on any perspective listener event.
The
uniqueId
can be used later to remove the listener instance or to overwrite one listener with another one by using the same ID value.
The code to register the part listener looks quite similar:
var conn=X2E.createConnection();
var pUI=com.x2e.PlatformUIAPI.getUI(conn);
var wb=pUI.getWorkbench();
var activeWindow=wb.getActiveWorkbenchWindow();
var partService=activeWindow.getPartService();
//pass a Map to the listener, used for data exchange
var dataMap=new java.util.Hashtable();
sessionScope.partListenerContent=dataMap;
//read information about current viewpart and store it
//in the dataMap that will be passed to the listener code
var activePart=partService.getActivePart();
if (activePart) {
dataMap.originatorPrimaryId=activePart.getSite().getId();
if (activePart instanceof com.mindoo.remote.api.org.eclipse.ui.IRemoteViewPart) {
dataMap.originatorSecondaryId=activePart.getViewSite().getSecondaryId();
}
}
//unique id, can be used to remove listener
var uniqueId="x2esamples-listeners";
//location of SSJS library
var dbPath=database.getFilePath();
var dbServer=database.getServer();
var libraryName="Listeners";
var methodName="partListener";
//show all listener events
var enableBitMask=-1;
var resultList=new java.util.Vector();
dataMap.put("result", resultList);
partService.addPartListenerSSJSLib(enableBitMask, uniqueId, dbServer, dbPath, libraryName,
methodName, dataMap);
It's safe to use the same uniqueId
here, because the ID only needs to be unique per listener type.
To remove the listener, only the uniqueId
parameter is required:
var conn=X2E.createConnection();
var pUI=com.x2e.PlatformUIAPI.getUI(conn);
var wb=pUI.getWorkbench();
var activeWindow=wb.getActiveWorkbenchWindow();
//unique id to specify the listener instance that should be removed
var uniqueId="x2esamples-listeners";
activeWindow.removePerspectiveListenerSSJSLib(uniqueId);
And similar code to unregister the part listener:
var conn=X2E.createConnection();
var pUI=com.x2e.PlatformUIAPI.getUI(conn);
var wb=pUI.getWorkbench();
var activeWindow=wb.getActiveWorkbenchWindow();
var partService=activeWindow.getPartService();
//unique id to specify the listener instance that should be removed
var uniqueId="x2esamples-listeners";
partService.removePartListenerSSJSLib(uniqueId);
The listener code is stored in SSJS libraries. The database server/filepath, library name and JavaScript method name need to be provided when the listener is registered.
Although the listener notification is done in a background job, so that the Notes client UI stays responsive, it is recommended to limit the listener code to the lowest possible execution time and performance impact. In addition, you should make use of the "eventBitmask" parameter to register the listener only for specific event types, if not all types are interesting for your use case.
XPages2Eclipse will notify all registered listeners in one single loop. That means that by writing a bad listener implementation, you will influence other implementations and cause an unnecessary delay.
Our JavaScript code extracts the most interesting values from the activated part/perspective and writes the result as a Vector entry into the data
object, which is the Hashtable
instance that we created for the listener registration.
//this method is called by the registered PartListener; it receives
//events when the state of viewparts gets changed (e.g. activated/deactivated)
function partListener(uniqueId, eventBit, part, data) {
var resultList=data.get("result");
var partTitle=part.getTitle();
var partSite=part.getSite();
var partId=partSite.getId();
var pluginId=partSite.getPluginId();
var secondaryId="";
if (part instanceof com.mindoo.remote.api.org.eclipse.ui.IRemoteViewPart) {
var viewSite=part.getViewSite();
secondaryId=viewSite.getSecondaryId();
}
var sType;
if (eventBit==
com.mindoo.remote.api.org.eclipse.ui.IRemotePartService.PARTACTIVATED) {
sType="Activated:";
}
else if (eventBit==
com.mindoo.remote.api.org.eclipse.ui.IRemotePartService.PARTBROUGHTTOTOP) {
sType="BroughtToTop:";
}
else if (eventBit==
com.mindoo.remote.api.org.eclipse.ui.IRemotePartService.PARTCLOSED) {
sType="Closed:";
}
else if (eventBit==
com.mindoo.remote.api.org.eclipse.ui.IRemotePartService.PARTDEACTIVATED) {
sType="Deactivated:";
}
else if (eventBit==
com.mindoo.remote.api.org.eclipse.ui.IRemotePartService.PARTOPENED) {
sType="Opened:";
}
if (sType) {
var info=sType+
"title="+partTitle+", "+
"partId="+partId+", "+
(secondaryId ? "secondaryId="+secondaryId+", " : "")+
"pluginId="+pluginId;
resultList.add(info);
//limit the list to 50 entries
while (resultList.size()>50) {
resultList.remove(0);
}
updateXPage("part", data);
}
}
//this method is called by the registered PerspectiveListener; it gets
//called when perspectives (main tabs in the Notes client) get activated
function perspectiveListener(uniqueId, eventBit, page, perspectiveDesc, changeId, data) {
var resultList=data.get("result");
var perspId=perspectiveDesc.getId();
var perspLabel=perspectiveDesc.getLabel();
var sType;
if (eventBit==
com.mindoo.remote.api.org.eclipse.ui.IRemotePageService.PERSPECTIVEACTIVATED) {
sType="Activated:";
}
else if (eventBit==
com.mindoo.remote.api.org.eclipse.ui.IRemotePageService.PERSPECTIVECHANGED) {
sType="Changed:";
}
if (sType) {
var info=sType+
"perspectiveId="+perspId+", "+
"perspectiveLabel="+perspLabel;
if (changeId) {
info=info+", changeId="+changeId;
}
resultList.add(info);
//limit the list to 50 entries
while (resultList.size()>50) {
resultList.remove(0);
}
updateXPage("perspective", data);
}
}
Code execution on XPages viewparts
Both listener methods call a function updateXPage
. Here, we are using the XPages API of XPages2Eclipse to locate the XPages viewpart and execute a piece of client side JavaScript in the context of the HTML page to notify the application about the new events.
//this method tries to locate the XPages viewpart in one of the workbench
//pages and executes a piece of JavaScript code through the XPages Xulrunner
//browser APIs in order to notify the page about new events that have been
//logged in the shared result list
function updateXPage(updateType, data) {
var xspPartId=data.get("originatorPrimaryId");
var xspSecondaryId=data.get("originatorSecondaryId");
if (xspPartId && xspSecondaryId) {
var pUI=com.x2e.PlatformUIAPI.getUI(eclipseconnection);
var wb=pUI.getWorkbench();
var activeWindow=wb.getActiveWorkbenchWindow();
var wbPages=activeWindow.getPages();
for (var i=0; i<wbPages.length; i++) {
//iterate over all open workbench pages
var currPage=wbPages[i];
//search for a view with the specified primary and secondary id
var viewRef=currPage.findViewReference(xspPartId, xspSecondaryId);
if (viewRef) {
//view reference found; now try to access the actual viewpart
var part=viewRef.getView(false);
if (part) {
var xspTools=com.x2e.XPagesAPI.getTools(eclipseconnection);
//use XPages tools to check if this viewpart really is
//an XPages viewpart
if (xspTools.isXspViewPart(part)) {
//now it's safe to call the other XPages tools methods:
//var xspUrl=xspTools.getUrl(part);
//debug("URL: "+xspUrl);
//var xspDom=xspTools.getDomAsXML(part);
//debug("HTML: "+xspDom);
var jsCode="if (window.updateNotification)"+
"{window.updateNotification('"+updateType+"') }";
xspTools.execute(part, jsCode);
return;
}
}
}
}
}
}
As you can see in the code snippet, there are two more interesting methods in the XPages API: one to read the current URL and another one to grab the HTML DOM content as a string. The latter might be useful if you need to extract data from an existing XPages application and do not have enough access rights to modify its source code.
Part activation history
If you are just interested in tracking the last N viewparts, writing a custom listener is not the ideal solution. For this purpose, we have added a simple method to the
IPartService
class that maintains a history of the last 20 visited viewparts.
The main use case for this method is to give you an easy way to find the viewpart that was active right before a user clicks on an XPages sidebar application - because that click moves the focus to the sidebar so that calling IPartService#getActivePart()
always returns the sidebar component instead of the view or document the user was working in before.
Here is the code for the "refresh" action of the third text field in the sample application, which shows you how to use the activation history:
var conn=X2E.createConnection();
var pUI=com.x2e.PlatformUIAPI.getUI(conn);
var wb=pUI.getWorkbench();
var activeWindow=wb.getActiveWorkbenchWindow();
var partService=activeWindow.getPartService();
var partHistory=partService.getLastActiveParts();
var txt="";
//go through list of recently activated parts
for (var i=0; i<partHistory.size(); i++) {
//the list contains workbench part references (IRemoteWorkbenchPartReference)
var currPartRef=partHistory.get(i);
var primaryId=currPartRef.getId();
var secondaryId;
if (currPartRef instanceof com.mindoo.remote.api.org.eclipse.ui.IRemoteViewReference) {
secondaryId=currPartRef.getSecondaryId();
}
//generate output
if (txt!="")
txt+="\n";
txt+="primaryId="+primaryId;
if (secondaryId) {
txt+=", secondaryId="+secondaryId;
}
var title=currPartRef.getTitle();
txt+=", title="+title;
var wbPart=currPartRef.getPart(false);
if (wbPart) {
if (secondaryId) {
//this reference belongs to a viewpart
var pluginId=wbPart.getViewSite().getPluginId();
if (pluginId) {
txt+=", pluginId="+pluginId;
}
}
txt+=", state=opened";
}
else {
txt+=", state=not opened";
}
}
getComponent("wbPartHistory").setValue(txt);
The currently active part is the first entry in the part activation history list.
Code Caching
For performance reasons, the SSJS library content is read only once, when the listener is registered. Remove and re-add the listener to force a reload of the code. If you are using custom Java classes/resource in your JavaScript code that are located in the library's database design, you can flush the NSF classloader cache with the
x2ecl
OSGi commands described in the
NSF Classloader reference.
The sample application can be downloaded
here.