Archive | January 2013

Prevent a Lag when Refreshing an Embedded View in XPages

Have you gotten frustrated with delays in refreshing a view embedded on an XPage? Fortunately, there’s an easy fix.

I recently worked on an application that had an embedded form and view for related documents displayed within a main form. The embedded view used a category filter to only display documents related to the main form, based on a shared ID.

The save button on the embedded form was set to partially refresh the embedded view. However, when a document was created via the embedded form, the view wasn’t immediately updated with that new document.

When I would add or edit another document via the embedded form (thus triggering the refresh on the view again), the view would then show the first document that was added. It was always one refresh behind when adding a new document.

I discussed it with Paul Withers, and we determined that the new documents weren’t showing up when the view was refreshed due to a timing issue. The view index wasn’t being updated fast enough for the new document to appear in the view when refreshed on screen.

Fortunately, there’s a pretty simple solution. It uses a method similar to technique I described in a recent post — getting a handle to the underlying view and acting upon it.

Once we have that handle, we can force it to refresh, so that when the partial refresh is executed, the underlying view’s index is up-to-date and the new document will be included in the updated view on screen.

To put it in practice, just include this line on the ‘Save’ button that saves the new document from the embedded form (and ensure that the button is set to partially refresh the view panel).

getComponent("ViewNameHere").getDataSource().refresh()

That’s all there is to it!

Advertisements

var = != var ==

This is a public service announcement for anyone rewriting LotusScript code in Javascript (client-side or server-side) for an XPages application.

If you want to check whether your variable (x) has a value of ‘IE Stinks’, be careful to use the correct operator.

This line, will always return true in JavaScript (and not just for the obvious semantic reason):

if (x = 'IE Stinks')

This line is the correct line to use:

if (x == 'IE Stinks')

In JavaScript, the equals sign (=) is only an assignment operator, which means it assigns the value on the right to the variable on the left. Even though syntactically similar, it is a mistake to use it as a comparison operator (comparing whether one value equals another), because it will always return true.

In LotusScript, the equals sign is both a comparison operator and an assignment operator, which means we can use it in both cases. This is why it’s easy to make that mistake.

So, to recap, IE Stinks and these are the correct operators for assignment and comparison in LotusScript and JavaScript:

Assignment Comparison
LotusScript x=’IE Stinks’ if (x=’IE Stinks’)
JavaScript x=’IE Stinks’ if (x==’IE Stinks’)

Getting the Most out of the XPages View Panel Control Part 4: Use Dojo to Fix Category Indenting

If you’ve ever created a view panel for a categorized view that includes a Totals column, you’ve probably noticed that the view is not indented properly. This is pretty common to find on modernization projects where views from an existing Notes client application are being surfaced in XPages. In this article, I’ll show how you can fix that with client side JavaScript and dojo.

Standard Categorized View Indentation

I created a categorized view with a long category value for my database of NFL football teams. When I create a view that just includes that category column, along with the location and team name fields, it indents the way I would expect.

CategorizedView_NoTotals_A

Categorized View with a Totals Column

However, if I add a totals column to the view and create a view panel for the view, the formatting is pretty awful — especially if I have a long category value.

CategorizedView_WithTotals_B

Since it has to display something else (the totals column) in the category row, it breaks the whole row up into cells that correspond to each column.

<tr>
<td class="xspColumnViewStart">
<div class="xspColumnViewStart">

{Source for expand/collaps button removed}

<a id="view:_id1:viewPanel1:0:viewColumn1__shrink:1link" style="cursor:pointer;" class="xspColumnViewStart">
American Football Conference - East Division</a>
</div>
</td>
<td class="xspColumnViewMiddle"></td>
<td class="xspColumnViewMiddle"></td>
<td class="xspColumnViewEnd">
	<span id="view:_id1:viewPanel1:0:viewColumn4:_internalViewText" class="xspTextViewColumn">4</span>
</td>
</tr>

Compare this with the source of the category row in the first screen shot above, where you can see that it sets the colspan attribute to cover all cells in the row, with provides proper indentation.

<tr>
<td colspan="3" class="xspColumnViewStart">
<div class="xspColumnViewStart">

{Source for expand/collapse button removed}

<a id="view:_id1:viewPanel1:0:viewColumn1__shrink:1link" style="cursor:pointer;" class="xspColumnViewStart">
American Football Conference - East Division</a>
</div>
</td>
</tr>

Dojo to the Rescue

So, what we need is a way to get the category column to span across multiple cells so that the values below it can be indented properly. Fortunately, with no extra configuration, we have dojo libraries available.

Stay Classy

The cleanest way I’ve found to help the necessary code identify the category cells easily is to apply a class to the category column in the view.

However, we need to apply it conditionally, because the class will be applied to the first table cell in every row in the view — even the ones that aren’t categories.

On the category column, I set the style class’s computed value to this:

if (rowHandle.isCategory()) {
	return 'category';
} else {
	return '';
}

This assumes that I have set my view panel’s var to rowHandle.

Code to Fix the Indentation

XPages and Custom Controls have an onClientLoad event that we can use to run code to process the view after its loaded.

This code will work on each categorized row. It will remove the blank cells between the category value and the totals column and add a colspan attribute to the category cell to use up the blank space, thereby indenting the rows below more naturally.

// Get a list of all rows in the view panel.
dojo.query('.xspDataTableViewPanel table tr').forEach(function(nodeRow, indexRow) {

  // Locate the category cells within the context of the view rows
  dojo.query('td.category', nodeRow).forEach(function(nodeCat){

    // Execute a search on the parent node (row) and remove all cells until data is found
    var emptyCells = 0;
    var keepCounting = true;
    dojo.query('td', nodeRow).forEach(function(nodeTD, indexTD){

      // Check all non-category cells until a non-empty cell is found
      if ((keepCounting) && !dojo.hasClass(nodeTD, 'category')){
        if (nodeTD.innerHTML == '') {
          emptyCells +=1;
          dojo.destroy(nodeTD);
        } else {
          keepCounting = false;
        }
      }
    });

    // Add a colspan attribute to the category cell (1 + [# of empty cells])
    dojo.attr(nodeCat, 'colspan', 1+emptyCells);
  });

});

Here is the result:

CategorizedView_WithTotals_C

Handle View Paging

The onClientLoad code works great when the view first loads, but the problem is that views are generally updated with partial refreshes as the user pages through, so we need to run the code on that event as well.

One option is to trigger a full refresh when the view is paged through, by selecting the view pager and disabling the partial refresh option, but there’s a better way.

Using the technique I demonstrated in my last post, all I need to do is move the onClientLoad event handler into the view panel and then this code will run when the page is loaded and when the user pages through the view.

Have you tackled this problem in a different way? If so, I’d love to hear how you did it.

Triggering Client-Side JavaScript While Paging Through an XPages View Panel

If you have a situation where you need to trigger client-side javascript code while paging through a view panel, you’re in luck.

I recently worked on a technique that required using dojo code to modify a view panel. With the code in the page’s onClientLoad event, the code worked fine when the view first loaded. However, it did not fire again as I paged through the view panel.

One option was turning off the partial refresh setting on the view pager, but I wanted to find a better solution that did not hurt application performance.

While looking through the XPages Control Documentation on openNTF, I did not see any events on any of the classes in the hierarchy for the xp:viewPanel control, but I did notice that the xp:panel control has an onClientLoad event. Granted, they don’t share the same inheritance, but it at least gave me the idea to see if I could manually add an onClientLoad event to the view panel.

It worked!

Adding Your Own onClientLoad Event to a View Panel

All I had to do to make my solution work was to move the page-level onClientLoad event into my view panel. Now it triggers when the view loads initially and then re-loads with every click of the pager.

To try this out, create a page with a view panel and add a single line of javascript to the onClientLoad event of the page.

The last few lines of your page’s source should look like this:

      </xp:viewColumnHeader>
    </xp:viewColumn>
  </xp:viewPanel>

  <xp:eventHandler
    event="onClientLoad"
    submit="false"
  >
    <xp:this.script><![CDATA[alert('loaded');]]></xp:this.script>
  </xp:eventHandler>	
</xp:view>

You can test your view in the browser and you’ll see that the message is displayed when you first load the page, but not as you page through the view.

Now, move the event handler (including the surrounding <xp:eventHandler> tags) up a line so it’s before the closing </xp:viewPanel> tag (but after the last </xp:viewColumn> tag).

Now, the last few lines of the page source will look like this:

      </xp:viewColumnHeader>
    </xp:viewColumn>
    <xp:eventHandler
      event="onClientLoad"
      submit="false"
    >
      <xp:this.script><![CDATA[alert('loaded');]]></xp:this.script>
    </xp:eventHandler>
  </xp:viewPanel>	
</xp:view>

You can test in the browser and now you’ll see the message pop up when the page is first loaded and every time you click on the view pager.

Maintaing the Code

The downside is that now you don’t have the script editor to edit the code any more (since this event isn’t exposed in the properties panel), so, at minimum, I would recommend implementing and debugging it on the onClientLoad event for the page and then moving it into the view panel when it’s ready.

A better option would be to make the event a single line that calls a script library function as needed, which is a better design for reuse across multiple views.

Up Next

In my next post, I’ll use this technique as part of a solution for fixing view column indentation.

Getting the most out of the XPages View Panel Control – Part 3: Implementing Field-Specific Search Functionality

In part 2 of this series, I showed how you can implement a dynamic list of Jump To options in a standard View Panel control. In this post, I’ll demonstrate is how to provide field-specific full-text search capability in a view.

Implementing standard full-text search

Implementing full-text search capability has been documented since before most of us ever heard of XPages in Declan Lynch’s ‘Learning XPages’ tutorial, so I won’t focus on it in as much detail here.

At a high level, you can implement full-text searching on a view panel with these three steps:

  1. Add a search field to your page and bind it to a scope variable.
    ViewPanel_3A
  2. Update the view panel’s data source to use the value of that scope variable in the search property
    ViewPanel_3B
  3. Add a button next to the search field that triggers a partial refresh on the view panel

This will execute the search in your view and display the results on the same page.

Of course, the database is full-text indexed, if not, you’ll get an error like this:

ViewPanel_3C

Enhancing the search to be field-specific

We can pretty easily take this a step further and allow our users to perform field-specific searches.

To do so, we’ll need to take these three steps:

  1. Add a combo box with a list of options for the field to search.
  2. Remove the binding of the search field to the scope variable (if you already have it in place)
  3. Update the Search button to build the search string and set it in the scope variable that the view is expecting

Building the list of options

Create a combo box with the list of search options. Make the value of each option correspond to the actual field name on the documents. Otherwise, you’ll need a little bit more code on the Search button to map the selection to the appropriate field name.

Also, be sure to include an ‘All Fields’ option to allow a search across all fields.

Building the search string

The key to enhancing the search from being generic full-text to being field-specific lies in the search string syntax. If you’ve done this before with LotusScript, you’ll find that you already know it!

To search my database of NFL teams for all teams in the National Football Conference (NFC), I would need the search string to look like this:

FIELD Conference CONTAINS NFC

To search the database for all teams that are located in a city that starts with the letter P, I would need this search string:

FIELD Location CONTAINS P*

Instead of requiring that the users type the precise full value of the field that they are searching, I generally append an asterisk to the search in order to provide the results that the user is most likely expecting.

The Search Button Code

Now that this is all set up, we just need to add some code to the Search button to build the search string and store it in the scope variable.

var field = getComponent('cbField').getValue();
var search = getComponent('txtSearch').getValue();
var query = '';
if (field == 'All Fields') {
	query = search + '*';
} else {
	query = 'FIELD ' + field + ' CONTAINS ' + search + '*';
}
viewScope.searchString = query;

Line 1 gets the selected field to search (my field’s ID is cbField)

Line 2 gets the search string (my field’s ID is txtSearch)

Lines 4-5 handle the generic full-text search for the case when a specific field is not selected

Line 7 builds the field-specific search string and appends the asterisk to provide more predictable results

Line 9 stores the search string in the scope variable that the view will use to filter the results

Here is the final result:

ViewPanel_3D

Now, I can open my view and easily perform a search for all teams who are located in a city that starts with the letter P.

ViewPanel_3E

This is a simple example, in a larger database, you will likely need to build the form name into your search, by including ‘FIELD Form CONTAINS form_name’ in the search string.

Further Enhancement

Once you know the syntax to building the full-text search, there are numerous ways you could change the interface for it, depending on your needs. You could build a search form that provides many more options for field-specific searches. You could also provide a series of combo boxes for the user to select multiple parameters to filter the view. You could even populate the combo boxes dynamically based on data in the view, using a concept similar to the one I showed in part 2 of this series.

Getting the most out of the XPages View Panel Control – Part 2: Adding a dynamic ‘Jump To’ feature

In part 1 of this series, I showed how you can pass client-side JavaScript through a view column in a standard View Panel control. In this post, I’ll demonstrate is how to provide “jump to” capability in a view, with a dynamically-built list of options.

The startKeys property

If you’ve used the view panel, you may be aware that it has a startKeys property. To locate it, select the view panel, click on the All Properties subtab, and you’ll find it under data > data > startKeys.

ViewSeries_2_a

Enabling the user to jump to a selected starting value

In the screen shot above, I hard-coded it to the letter ‘C’. When I load my view, it will start with the first value that begins with that letter.

Of course, we need to make this dynamic so the user can update the view as needed. The simplest way to do so is to set the startKeys property to the value of a scope variable and then add a drop-down list that is bound to that scope variable.

To set the startKeys property to the value of a scope variable, click on the blue diamond in the startKeys property and add this server-side JavaScript statement:

return viewScope.get('viewStart');

Next, create a combobox above the view panel and give it a few hard-coded values, such as A, B, C, and D. The key is to click on the Data tab for the combo box and set the data binding to the same scope variable that startKeys property will return.

ViewSeries_2_b

Finally, update the onchange event of the combo box to trigger a partial refresh on the view panel.

ViewSeries_2_c

So, when the user selects a letter to jump to, the selected letter is stored in a scope variable. The view panel is refreshed and it looks to that scope variable to determine where to start the display of the view.

Populating the list of Jump To options dynamically

This is all well and good, but let’s take it a step further. You can provide all of the choices in the alphabet by manually populating the drop-down list. However, if your data doesn’t have all of the options, this may provide unexpected results. For example, if the user selects X, but there aren’t any rows in the view that start with X, the view will start at the beginning.

To eliminate potential confusion, we can dynamically populate the list with only valid options, based on our view data.

To do so, we can run a script that builds a list of valid options and stores it in a scope variable, then update our Jump To combobox to get its list of options from that scope variable.

Execute the below code on the beforeRenderResponse event of the XPage or custom control that contains the view. This allows the code to have access to the view, but still execute before the page is displayed to the user. (It will fail on the getComponent() statement if run on the beforePageLoad event, because the view isn’t yet available at that point.)

var viewName = getComponent('viewPanel1').getData().getViewName();
var vw = database.getView(viewName);
var colNum = 1;
var cols:Vector = vw.getColumns();
for (var i=0; i < cols.length; i++) {
	if (cols[i].isSorted() && !cols[i].isHidden()) {
		colNum = i + 1;
		break;
	}
}
var letters = @DbColumn(null, viewName, colNum);
var options = @Trim(@Unique(@UpperCase(@Left(letters, 1))))
viewScope.put('jumpToOptions', options);

Line 1 gets the name of the view that is the source of the view panel’s data. It is important to note that the data source must be defined within the view panel in order for getData() to work.

Line 2 gets a handle to the back end view. This can be enhanced if the source view exists in a separate database

Lines 3-10 look for the first visible, sorted column in the view. If the view doesn’t have any hidden columns, it would work to assume the first column is the key column, but this code is more dynamic so it can be reused across more views.

Lines 11-12 get a list of unique options, based on the data in the first column of the view.

Line 13 stores that list of options in a scope variable.

The last step is to set the combo box’s value to this computed formula:

return viewScope.get('jumpToOptions');

The result is that the Jump To list only includes valid options in the view.

ViewSeries_2_d

Caveats

Be aware that once a starting value is selected, the user will not be able to page backward to see any data in the view before that point. A good way to work around this is to include a blank option at the top of the Jump To list, which will reset the view to start at the beginning.

Of course, performance is also a concern. There may be a noticeable lag when this logic is used on a large data sets or a view with documents including Readers fields. The performance could be improved by caching the values in a sessionScope variable or by running a background agent that stores the list of available options on a configuration document and updates it periodically throughout the day.

Alternatives

Have you implemented similar functionality in a different way? Let me know — I’d love to hear about it.

Up Next

In the next post in this series, I’ll show how to implement field-specific full-text searching across a view.