Wednesday, April 28, 2010

XPages: Code snippet for number/string conversion

The DecimalFormat object is cached in the application scope due to performance/memory concerns.

The default locale for the conversion seems to be the locale of the OS on the server, so if you want to support multiple locales, you'd have to extend the functionality.

Example usage (Norwegian locale):
String to number
NumberConverter.stringToNumber( '15,55', '0.00' ) -> java.lang.Double

Number to string
NumberConverter.numberToString( 15.926, '0.00' ) -> "15,93"

var NumberConverter = {
stringToNumber : function(numberString, patternString) {
try {
if( !numberString ){ return null; }

var formatter = NumberConverter.getFormatter(patternString);
return formatter.parse(numberString);
} catch( e ){ Debug.logException( e ); }
},
numberToString : function(number, patternString) {
try {
if( !number ){ return null; }

number = new java.lang.Double(number);
var formatter = NumberConverter.getFormatter(patternString);
return formatter.format(number);
} catch( e ){ Debug.logException( e ); }
},
getFormatter : function(patternString) {
try {
var cacheKey = 'numberFormatter' + patternString;
var numberFormatter = applicationScope[cacheKey];
if(!numberFormatter) {
numberFormatter = java.text.DecimalFormat(patternString);
applicationScope[cacheKey] = numberFormatter;
}

return numberFormatter;
} catch( e ){ Debug.logException( e ); }
}
}

Tuesday, April 27, 2010

XPages: Server Side Event Delegation - In view edit

This is a continuation of the previous investigation into the possibilities of Server Side Event Delegation (SSED).

As I mentioned at the end of the previous blogpost, the demoapp didn't take advantage of Client Side Event Delegation (CSED). This resulted in having to create the same amount of client side event listeners as was generated by the server. If you have repeat controls/view events, this can amount to quite a bit of script tags (html to download). The event listeners themselves can take up quite a bit of memory, and may make the page act a little sluggish.

Here's the generated script tags for events in this demoapp:
XSP.addOnLoad(function() {
  XSP.attachPartial("view:_id1:eventDelegator", "view:_id1", null, "foo", ...
  XSP.attachPartial("view:_id1:peopleView:pager1__Group__lnk__1", null, ...
  XSP.attachPartial("view:_id1:peopleView:pager1__Group__lnk__2", null, ...
  XSP.attachPartial("view:_id1:peopleView:pager1__Group__lnk__3", null, ...
  XSP.attachPartial("view:_id1:peopleView:pager1__Group__lnk__4", null, ...
  XSP.attachPartial("view:_id1:peopleView:pager1__Next", ...
});

One for the global event delegator event handler, and one per link in the view pager. The view has thirty "click to edit" cells, and a couple of buttons from the previous version. Not bad. :)

The experiments into SSED is partially due to the aforementioned reasons, curiosity, and my eternal struggle to find smarter ways to work. Only time will show if it fulfills all of the above.

Since the previous experiment, the demoapp has grown a bit. I've added a couple of more class attributes:
  • delegate-click-parent: makes a component a clickable area
  • click-trigger: when parent has delegate-click-parent class/styleClass, elements with this class, when clicked, triggers the server side event delegator
  • refresh-[refreshId]: previously refresh-target-[refreshId], the target of the partial refresh
  • action-[name]: used by the server side event delegator to determine what action to take
  • data-[value]: can be used on the server side for various things. E.g. unid of a row in a view
The editable columns work like this:
The view has the delegate-click-parent class. This makes all the child elements with a class attribute of click-trigger activate the server side event delegator.

I created a custom column. This lets me put other controls into the column.

Inside the column, I put a panel, a field and a button. A cell is editable when a scoped variable containing unid is the same as the unid of the row that the cell belongs to.

The panel has a computed styleClass attribute. When the cell isn't editable, the panel acts as a click trigger. One of the class attributes is the previously mentioned data-class. This contains the unid of the row the cell belongs to. This unid is put into the previously mentioned scoped variable, making the cell editable.

The field has value="" and defaultValue="#{row.columnName}". This makes it possible to edit the field/have it take the value from the appropriate row/column. The field has a readonly attribute that's computed based upon if the cell is editable.

The button is hidden when the cell isn't editable. When clicked, it triggers a save and resets the editable row. The client side id of the button is passed along to the server. This makes it possible to get the name of the field/get the value from the params-map (contains all submitted values).

E.g.
component id of field/button -> field/button
client side id of button: view:_id1:button
client side id of field: view:_id1:field

No more than a simple replace to find the correct field name :)

That's more or less it. If this sounds like something interesting, take a look at the demoapp/code, and leave a comment if you're curious about something.

>> Download Demoapp

Share and enjoy!

Thursday, April 22, 2010

XPages: Server Side Event Delegation

Update 27.04.10: I've updated the demoapp with In View Edit functionality

While working on a view with in view edit functionality today, I was appalled over how much client side code was generated.

I guess my subconsciousness was working on the problem, as I came up with a hack that lets you delegate events, while eating dinner (almost forgot the comma). It's somewhat similar to the event delegation that you would do in a browser. The current implementation has its definite limits, but I think it's a nice proof of concept.

Without the Programmatically Triggering an XPages Server Side Event Handler from a Client Side Script article by Jeremy Hodge, I don't think I would have come up with this hack, so thank you for sharing, Jeremy!

The short and simple explanation of the hack:
A global event handler is added to the XPage:
<xp:eventHandler id="eventDelegator" event="foo"
  action="#{javascript:delegateEvent()}" submit="true" refreshMode="partial"
  refreshId="#{javascript:param.$$xspsubmitvalue}" />

I use foo as the event name, so that the generated client side event handler has no possibility of being triggered.

I've added some client side JavaScript that acts upon elements with certain class attributes.
delegate-click: Clicks on an element with this class triggers the server event handler.
refresh-target-componentId: The component id of the control that should be updated.

The code could easily be extended to handle other events. Out of curiosity, I tested onmouseover and onfocus. Both work great.

When an event is triggered, param.$$xspsubmitvalue is set to the id of the component I want to update. As you can see in the above XPages source code snippet, the refreshId of the event is computed to this value. This means that only the requested component is updated. The eventDelegator-function that is triggered by the event uses also uses this value to determine what server side action should be executed.

In the attached demoapp, a viewScope value is altered depending on the refreshId. This toggles a couple of panels, which have computed their visibility based upon the scoped variable.

As for the limitations..

The component that is the target of the refresh has to be visible, as its generated id (clientId) is necessary to get the partial update to work.

This version of the hack doesn't do much about the problem of a lot of active event listeners for my "in view edit" problem. It only results in less generated code. To fix this, I'd have to make the client side code smarter. E.g. add client side event delegation. The container could have a delegate-child-clicks class. The children that should fire the events could have maybe have a click-trigger class. Anyways, that's for a future proof of concept.. :)

The reason I decided to post this experiment is that I hope that it can inspire other XPages developers to make something cool, as I feel Jeremy's post did for me.

Here's the demoapp for you to probe and inspect

Share and enjoy!

Thursday, April 8, 2010

XPages: Code snippet for date/string conversion

The SimpleDateFormat object is cached in the application scope due to performance/memory concerns (I have data tables generated from JSON with up to several thousand conversions).

Example usage:
String to date
DateConverter.stringToDate( '12.12.2001', 'dd.MM.yyyy' ) -> java.util.Date

Date to string
DateConverter.dateToString( @Now(), 'dd.MM.yyyy hh:mm' ) -> 08.04.2010 12:01

var DateConverter = {
 dateToString: function( date:java.util.Date, pattern:String ){
  try {
   if( !date ){ return ''; }
  
   var formatter = DateConverter.getFormatter( pattern );
   return formatter.format( date );
  } catch( e ){
   // ErrorHandling
  }
 },
 
 stringToDate: function( dateString:String, pattern:String ){
  try {
   if( !dateString ){ return null; }
  
   var formatter = DateConverter.getFormatter( pattern );
   return formatter.parse( dateString );
  } catch( e ){
   // ErrorHandling
  }
 },
 
 getFormatter: function( pattern:String ){
  try {
   var cacheKey = 'dateFormatter' + pattern;
   var dateFormatter = applicationScope[ cacheKey ];
   if( !dateFormatter ){
    dateFormatter = new java.text.SimpleDateFormat( pattern );
    applicationScope[ cacheKey ] = dateFormatter;
   }
   
   return dateFormatter;
  } catch( e ){
   // ErrorHandling
  }
 } 
}

Wednesday, April 7, 2010

XPages: Adding controls inside a pager

I haven't read anyone else writing about this, so I thought I should share this technique with you.

While working on a custom search data table, I wanted to show the number of entries in the result. I wanted the pagination links to be on the left of the pager bar, and the number of entries on the right side.

At first I started mucking about with CSS, but that ended up being a maintenance nightmare as the search control I was developing was to be used in a number of pages. I noticed that the pager control had the xp:key attribute. This attribute is also used by the editable area control that you can put inside a custom control. I noticed when adding multiple controls to the editable area, that the generated source code wrapped the controls inside a panel with an xp:key attribute.

Tried the same in the data table control and the view control, and it works. If you want to have your own controls in the header pager area, simply add an xp:panel-tag in the source code, with the attribute, xp:key="headerPager". If you want a custom footer, create an xp:panel-tag with the attribute xp:key="footerPager". If you already have a pager there, simply wrap it in the aforementioned panel, and remove the xp:key attribute from it, and it should work without the need for further modification.

Example of use:

Unfiltered view


Filtered view

Renaming fields with formula agents

Put the code below in a formula agent. Set the old field names in the _fieldNamesFrom-list, and the new field names in the _fieldNamesTo-list. Not sure if it works correctly for NotesRichTextItems, but it should work on the other field types.

If there isn't a field with the old fieldname, a new one isn't created (in case you need to run the agent multiple times -> doesn't blank the new fields).

_fieldNamesFrom := "silly_fieldname" : "tpyo_fieldname";
_fieldNamesTo := "crazy_fieldname" : "correct_fieldname";

@For( i := 1 ; i <= @Elements( _fieldNamesFrom ) ; i := i + 1 ;
 _valueFrom := @GetField( _fieldNamesFrom[i] );
 @If( _valueFrom = "" ; "" ; @SetField( _fieldNamesTo[i] ; _valueFrom ) );
 @SetField( _fieldNamesFrom[i] ; @Unavailable )
);
SELECT @All

I post this as much for myself as for you, as I tend to delete the renaming agents after they've done their duty.

Tuesday, April 6, 2010

I've added a comments feed

Just wanted to let you know that I've added an RSS feed for comments.

The feed