Tuesday, October 8, 2013

Configuring input field for DateTime picker with localized date/time format

In a new application a colleague of mine is working on, he wanted to let the user set date and time with Norwegian date and time format.

One would think it should be pretty straightforward, but it took me around three hours to figure out how to do it. I accidentally discovered the way to do it while making a custom control with one field for date and one field for time. The custom control was meant to combine the two values into a date/time value. Luckily there's a simpler solution.

Source for Norwegian Date/Time picker:
<xp:inputText id="dateTimePicker" value="#{document.date_time}">
 <xp:this.converter>
  <xp:convertDateTime pattern="dd.MM.yyyy HH:mm" type="both" />
 </xp:this.converter>
 <xp:dateTimeHelper>
  <xp:this.dojoAttributes>
   <xp:dojoAttribute name="constraints" value="{ datePattern : 'dd.MM.yyyy', locale: 'no', timePattern: 'HH:mm' }" />
  </xp:this.dojoAttributes>
 </xp:dateTimeHelper>
</xp:inputText>

I tested/this should work on Domino 8.5.3 and Domino 9.

Monday, September 23, 2013

Simple workaround for partial refresh issues with radio buttons

Update, 14.03.14: I had some issues with this technique when used inside a ExtLib dialog. I suspect this has something to do with the dialog having it's own form. To make it work inside the dialog, I added a parameter for the execId. If I remember correctly, execId specifies the part of the XPages tree that should be processed. It can be the same as the target of the refresh.

Example of use with execId:
XSP.partialRefreshPost( '#{id:targetOfRefresh}', { execId: '#{id:targetThatShouldBeProcessed}' } );

I had some issues getting partial refresh triggered by radio buttons to work as I wanted to across browsers. I found a simple workaround. Instead of specifying the partial refresh in the event handler, I set no submission for the server side part, and execute a slightly delayed XSP.partialRefreshPost from the client side event action:
<xp:radioGroup id="someId" value="#{document.someField}" defaultValue="yes">
 <xp:eventHandler event="onclick" submit="false">
   <xp:this.script><![CDATA[setTimeout( function(){
 XSP.partialRefreshPost( '#{id:targetOfRefresh}' );
}, 50 );]]></xp:this.script>
 </xp:eventHandler>
 <xp:selectItem itemValue="yes" itemLabel="Yes" />
 <xp:selectItem itemValue="no" itemLabel="No" />
</xp:radioGroup>

This seems to work nicely across the browsers I tested in, regardless of triggering the value change through the radio button or the label.

Thursday, September 19, 2013

Strange implementation of Document.setPreferJavaDates( true )

I wanted to try the Document.setPreferJavaDates today and see how it worked.

I called Document.setPreferJavaDates( true ); to ensure that no (Notes)DateTime objects were created as I didn't need them/didn't want to deal with recycling.

First I tried:
Document.getItemValueDateTimeArray( "fieldname" );
This returned a Vector of DateTime.

Then I tried:
Document.getItemValue( "fieldname" );
This also returned a Vector of DateTime.

The way that finally worked as expected (returned a Vector of java.util.Date) was this:
Document.firstItem( "fieldname" ).getValues();

I would imagine this is the least used method among developers wanting to get a date value from a field. This code was executed on a server running Domino 8.5.3FP2

Wednesday, July 3, 2013

Workaround for issues with XPage in iframe on external website

Update, 03.03.14: We had some further issues with Chrome and form submission. Code snippet updated with workaround.

A colleague of mine had some issues with an XPage running in an iframe on a customer's website. The customer notified us that some users with Chrome/Safari had issues with the page going blank.

When doing partial refresh, the page went into a reload loop. I did a quick check, and couldn't find anything wrong with the code. I believe it has something to do with cross domain cookies and Webkit.

My workaround was to have a script block that adds the SessionID parameter that was previously added in certain circumstances on earlier versions of XPages. The script block is only loaded for Chrome/Safari, and the code only runs when the XPage is in a frameset/iframe.

<xp:scriptBlock>
 <xp:this.loaded><![CDATA[${javascript:return ( context.getUserAgent().isChrome() || context.getUserAgent().isSafari() );}]]></xp:this.loaded>
 <xp:this.value><![CDATA[(function(){
// Fix for safari/chrome when page is in iframe. Cookie with SessionID seems to be discarded between requests
// Workaround: Set parameter for SessionID
var queryString = document.location.search;
var sessionIdParameter = 'SessionID=' + '#{javascript:return facesContext.getExternalContext().getRequest().getSession().getId();}';
if( self !== parent && queryString.indexOf( 'SessionID' ) === -1 ){
 if( queryString === '' ){
  document.location.search = sessionIdParameter;  
 } else {
  document.location.search = queryString + '&' + sessionIdParameter;
 } 
}

// Add SessionID parameter to form action if set in URL to preserve session
if( queryString.indexOf( 'SessionID' ) !== -1 ){ 
 var form = document.forms[0];
 if( !form ){
  return;
 }
 
 var formAction = form.action;
 var parameterDelimiter = '&';
 if( formAction.indexOf( '?' ) === -1 ){
  parameterDelimiter = '?';
 }
 form.action = form.action + parameterDelimiter + sessionIdParameter;  
}
})();]]></xp:this.value>
</xp:scriptBlock>

Friday, June 28, 2013

Benchmark: Fetching NoteIDs and documents from view

Update 28.06.13: I added a test - using the new NoteCollection.getNoteIds that was introduced in Domino 9.

In relation to an experiment I'm working on, I wrote a Java test agent to find the fastest way to get note id's from a view/fetching document values from documents in a view.

Here are the results from the benchmark:



All numbers are milliseconds. The code was run on a desktop computer (Core 2 Duo/Windows XP).

The benchmark code is written so that I think caching on the server is avoided between tests. What's interesting from the results is that it seems like it's marginally faster to fetch the note ids from the view/open the document by note id if you want to read values from the document.

If you want to run the benchmark in your application, here's the source code:
>> Download

The code is designed for running the test agent in a browser. Change the constants TEST_VIEW_NAME to the name of a view you want to test, and TEST_FIELD_NAME to a field name that the documents in the view have.

If you run the benchmark/get different results, let me know :)

Tuesday, May 21, 2013

Custom Control for custom layout of checkboxes

In a recent project, I wanted to create an interface to toggle different settings for different user types in an app. To save real estate on the page and (hopefully) make it more intuitive, I wanted to have a column per user type, and organize the checkboxes vertically.

I couldn't find any way to do this out of the box. To make it work, I made a regular checkbox group per field. Then in a repeat control, I have regular HTML checkboxes with name fetched from the real checkbox group. The checkbox group has to have the same values as the HTML checkboxes, or the values will be discarded.

The checkbox component is removed from the DOM via javascript when the page loads. If the real checkbox component isn't removed, you will only be able to select, and not deselect.

Screenshot from the demoapp (each checkbox column is bound to a multivalue field):


The custom control has four properties:
  • checkboxValues: Array of strings - Values for the checkboxes
  • fieldNames: Array of strings - Names of the fields in the form you want to connect the checkboxes to (remember to set them to multi-value in form)
  • headerLabels: Array of strings - Header labels for the checkbox columns
  • rowLabels: Array of strings - Labels for the checkbox rows

Currently, the custom control uses currentDocument to bind the fields. If the control doesn't work, you might need to use a more specific document data source.

Example usage
<xc:CheckboxTable checkboxValues="#{javascript:return ['inbox','outbox'];}"
   fieldNames="#{javascript:return ['hidden_items_administrators','hidden_items_users','hidden_items_limited_users'];}"
   headerLabels="#{javascript:return [labels.administrators, labels.users, labels.limitedUsers];}" rowLabels="#{javascript:return [labels.inbox,labels.outbox];}" />
The demoapp has been tested/works in these browsers: IE8/9 (should work in earlier browsers, but the layout is ugly), latest versions of Chrome, Firefox and Opera.

>> Download demoapp

The technique I used (real+"fake" checkboxes) should enable you to customize the layout of checkboxes however you want. I imagine the technique should also work for radio buttons.

Wednesday, March 20, 2013

Fix for partial refresh on Dojo Tab Container/Content Pane

I wrote this a while back, but I couldn't find that I'd shared it.

You need to use the partial refresh hijacker to use the code snippet.

This code snippet initializes Dojo Tab Containers/Dojo Content Panes in the area that's refreshed:
// Fix problem with partial refresh on Dojo Tab Container/Content pane
// Source for inspiration:
// http://www.openntf.org/projects/pmt.nsf/0/D228115FAA98DDEC86257A7D0050E7FF
dojo.subscribe( 'partialrefresh-complete', this, function( method, form, refreshId ) {
 var tabContainersAndContentPanes = dojo.query( '[id=' + refreshId + '] .dijitTabContainer[widgetid], ' +
  '[id=' + refreshId + '] .dijitContentPane[widgetid]' );
 if( tabContainersAndContentPanes.length === 0 ) {
  return;
 }
 
 for( var i = 0; i < tabContainersAndContentPanes.length; i++ ) {
  var widgetId = tabContainersAndContentPanes[i].getAttribute( 'widgetid' );
  var widget = dijit.byId( widgetId );
  if( widget ) {
   widget.startup();
   widget.resize();
  }
 }
} );

Add the code snippet in a dojo.addOnLoad function or something similar.

If you have other widget types that fails to initialize, just modify the selector/startup commands to fix.

Friday, January 18, 2013

Small LS class that can be used to check if fields have changed

We've had some issues with semaphore locks on one of our import databases. The import database has routines that import/update data, then replicate it to a cluster when it's done.

We're not exactly sure what triggers the locks, but the server crashed sometimes several times a day, so we decided to see if the import routines could be optimized to do as few writes as possible.

Several of the routines saved documents even if there were no field changes. I wrote a simple class to test for field value changes.
'// Used to test if certain fields have changed
Class FieldChangeChecker
 Private fieldsToCheck As Variant
 Private fieldValues List As Variant
 
 '// Run this after calculcation/etc. to see if specified fields have changed
 Function haveFieldsChanged( doc As NotesDocument ) As Boolean
  On Error GoTo bubbleError
  
  If doc.isNewNote Then
   haveFieldsChanged = True
   Exit Function
  End If
  
  Dim initialValue As Variant, currentValue As Variant
  ForAll fieldName In Me.fieldsToCheck
   initialValue = Me.fieldValues( fieldName )
   currentValue = doc.getItemValue( fieldName )(0)
   
   If initialValue <> currentValue Then
    haveFieldsChanged = True
    Exit Function
   End If
  End ForAll
  
  Exit Function
bubbleError:
  Error Err, errorMessage()
 End Function
 
 '// Comma separate field names. E.g. "forename,surname"
 Sub New( ByVal commaSeparatedFieldNames As String )
  On Error GoTo bubbleError
  
  Me.fieldsToCheck = FullTrim( Split( commaSeparatedFieldNames, "," ) )
  
  Exit Sub
bubbleError:
  Error Err, errorMessage()
 End Sub
 
 
 '// Run this before calculation/etc.
 Sub readInitialValues( doc As NotesDocument )
  On Error GoTo bubbleError
  
  ForAll fieldName In Me.fieldsToCheck
   Me.fieldValues( fieldName ) = doc.getItemValue( fieldName )(0)
  End ForAll

  Exit Sub
bubbleError:
  Error Err, errorMessage()
 End Sub 
End Class
Example of use:
..
Dim fieldChangeChecker As New FieldChangeChecker( "FirstName,LastName,Address,PostCode,City" )
Set customerDoc = customersView.getFirstDocument()
While Not customerDoc Is Nothing
 Call fieldChangeChecker.readInitialValues( customerDoc )

 '// Code that looks up the newest information and sets fields

 If fieldChangeChecker.haveFieldsChanged( customerDoc ) Then
  Call dataDoc.Save(True, False) 
 End If

 Set customerDoc = customersView.getNextDocument( customerDoc )
Wend
..
Regarding the errorMessage-function in the class. I use error bubbling in all my LS code. If something fails somewhere in a routine, I want the routine to stop executing. At the top of the stack, I use OpenLog/LogError. This gives me a nice pseudo stack of the function calls.
Code for errorMessage function:
Function errorMessage As String
 '// Simple function to generate more readable errors when dealing with error-bubbling
 Dim message As String
 message = Error
 
 If CStr( GetThreadInfo(10) ) = "INITIALIZE" Then
  errorMessage =  "Error " & Err & " on line " & Erl & " in function " & GetThreadInfo( 10 ) & ": " + Error
 Else
  errorMessage =  Chr(13) + Chr(9) + "Error " & Err & " on line " & Erl & " in function " & GetThreadInfo( 10 ) & ": " + Error$ 
 End If 
End Function