Search
Categories
Tags
Latest Comments

Entries in ssjs (5)

Wednesday
Jun022010

Avoid saving duplicate documents on browser refresh

Update 22.06.10: Parameters are carried over when the redirect happens.

A reader wanted to know if there was some way to avoid duplicate documents when saving a document, refreshing the browser, and pressing ok on the resulting dialog. I wasn't able to recreate the problem of duplicate documents, but here's a workaround for the "POST" dialog. It should also take care of any duplicate documents on browser refresh.

In JSF lingo, this is known as the POST-Redirect-GET pattern. This is quite easy to accomplish in XPages.

On the save button, put the save action, and an Execute Script action. In the script part, call on the function below (put it in a SSJS Script Library/add it to the page). This makes the server redirect to the document in a get request, and you avoid the double posting problem.

function redirectToCurrentDocument( switchMode:boolean ){
try {
if( typeof currentDocument === 'undefined' || viewScope.stopRedirect ){ return; }
// Gets the name of the XPage. E.g. /Person.xsp
var page = view.getPageName();

// Finds extra parameters, strips away XPages parameters/trims leading &
var parameters = context.getUrl().getQueryString().substring(1);
var extraParameters = parameters.replace(
/(action=\w{4}Document[\&]{0,1})|(documentId=[\w]{32}[\&]{0,1})/g, '');
    extraParameters = extraParameters.replace( /\&$/, '' );

// Gets the unid of the current document
var unid = currentDocument.getDocument().getUniversalID();

// Sets/changes the action according according to the mode the document is in
var isEditable = currentDocument.isEditable();
if( switchMode ){ isEditable = !isEditable; } // false -> true / true -> false

var action = ( isEditable ) ? 'editDocument' : 'openDocument';

// Open the current document via a get request
var redirectUrl = page + '?documentId=' + unid + '&action=' + action;
if( extraParameters ){ redirectUrl += '&' + extraParameters; }
context.redirectToPage( redirectUrl );
} catch(e){ /* Exception logging */ }
}
Friday
May282010

Binding XPages to External Databases - Watchout!

I'm working on a small app for my sales department.  They work offline in a totally disconnected state.  They have all their customer information in a series of Notes databases and they run 8.5.1 standard and replicate every night for updates. 

I've been asked to add a "Pricing Request" form to the application.  Since the long range plan is to convert the whole app to all XPages, I did not want to code this for the notes client.  I figured that I'd use XPages for this piece so it can run in the client or in the browser.  1 less thing to do in the future.

So I've been working on this app for a bit and have primarily been testing via the web browser.  The UI will not be fancy so I don't anticipate much difference between a browser and XPages In the Notes Client regarding any rendering issues.

Yesterday I wanted to give an "Alpha" version to a user so I can start getting some feedback.  So I go to test it in the Notes Client with the database running on the server and guess what?  It does NOT work.  It would not open the database that I was binding to!  It just made no sense. Of all the things that might not have worked the data binding was a problem?  Especially since it DID work for at least 1 database, but I couldn't get it to work for any others. 

I tried everything I could think of to fix it myself. I messed with ACLS....  I copied db's.... I moved db's out of sub-directories.... I said more then a few choice words.... I prayed to the Hockey Gods... I randomally deleted Users.... Nothing helped.

The original formula that I used to bind to the database was:

session.getDatabase(database.getServer(), "dbname.nsf")

Again - this worked great from the browser and for 1 database in the notes client.

When it became clear that I was dead in the water I reached out to friends on bleedyellow IM, twitter and email.  I talked to a couple of people who gave me good suggestions but no success.  Next would have been IQJam or a blog plea but I was too tired at that point.

This morning I woke up and came to work to find one of the best emails ever.  The XPage Genius's at IBM, Mr. Paul Hannan and Mr. Steve Castledine came to my rescue with an improved formula that seems to allow me to bind to a foreign database on the Web and in the Notes Client (Server and Local). I'm seriously considering having my wife engrave this on a plaque for me so I never forget it.

The formula is:

@Name("[CN]",@Subset(@DbName(),1))+"!!dbname.nsf"

Why this works when the other does not I do not know. I believe there already is an SPR for this issue and it's slated to be fixed in 8.5.2. I don't know if this has been blogged about before and I forgot it.  The syntax seems familiar so I'm sure I've seen it before somewhere.

A HUGE Thanks to Paul and Steve and everyone else I talked to for helping me!!!

Thursday
Apr152010

Programmatically Triggering an XPages Server Side Event Handler from a Client Side Script

Occasionally, I want to force an event handler to execute on the server in an XPage based on a condition I find during the execution of a client-side script. For example, I might have an object that the user is interacting with that is generated client-side, and therefore has no server-side defintion, and thus no server generated event handlers.

Well, there is a way to execute a server side event handler from a client script, and there is a lot of hidden power in this capability to boot.

Let's start with a simple example. Let's say you have a created a dijit.Toolbar at the top of your form that has a button. When the user clicks this button, you want to save the document. (Yes, normally you can just create an xp:div with a dojoType of dijit.Toolbar, and place a button on it in XPage markup and accomplish the same thing, but we're starting with an easy example, remember ;D ).

So lets start by creating that toolbar clientside with some javascript:

// create the new toolbar and add inside the body tag...
var myToolbar = new dijit.Toolbar({
    id:'myToolbar',
    style: {
       position: 'fixed',
       top:0,
       left:0,
       height:'22px',
       width:'100%'
    }
});

dojo.body.addChild(myToolbar);

Now, lets add a button to that toolbar:

myToolbar.addChild(new dijit.form.Button({
    label: 'Save Document',
    showLabel: true,
    id: 'saveDocumentButton',
    onClick: function() {
    }
}));

Ok, now that gets us a toolbar at the top of the page, with a button that says "Save Document", but doesn't do anything when clicked.

Now, let's look at the code we put in our XPage markup to code to create the event handler. So first we have to decide where to put our event handler. The simple rule, is where it makes sense! For example, in this case we are saving a document. So if the data source is defined in the XPage object, it would suit us fine to place the event handler right inside the XPage's xp:view tag, just below the data definition. If the data is defined in an xp:panel, then it makes sense that we would want it inside the xp:panel that defines the data document so we process the proper document. For this example, lets assume our data is defined at the XPage node, so we'll put our event handler just after our xp:this.data tag.

(pipes '|' are replacing the > and < since the blog is continually eating them)

     |xp:view .... |
        |xp:this.data|
             |xp:dominoDocument var="myDocument"
                  formName="myForm" action="editDocument"
                  documentId="#{java_script:viewScope.currentDocId}"|
             |/xp:dominoDocument|
         |/xp:this.data|

         |xp:eventHandler event="onfubar" id="eventhandler1a" submit="false"|
             |xp:this.action|
                 |xp:saveDocument||/xp:saveDocument|
             |/xp:this.action|
         |/xp:eventHandler|

        |!-- everything else goes here --|

    |/xp:view|

Ok, so there it is an eventHandler defined for the XPage, that really doesn't connect to anything. There are a few noteworthy items here. First, the event we specified. Normally this would be something like "onclick" or "onmousedown" ... In our case, we actually don't want to connect this event handler to anything, but we have to give it an event name, or we'll get errors, so we just specify a junk name of "onfubar".  Next, we specified an ID for the event handler, in this case "eventhandler1a".  We'll need to know this later on to specify which event handler we want to run (Yes that does mean you can have multiple event handlers defined, and can call any which one you want, more on that later.)

Now, let's look at the client side script used to execute this server side event handler.  They key to making this work is setting the value of the hidden field $$xspsubmitid to the id of the event handler we want to execute. Let's wrap it all up in a nice little function that we can call at any time:

var executeOnServer = function () {

    // must supply event handler id or we're outta here....
    if (!arguments[0])
        return false;

    // the ID of the event handler we want to execute
    var functionName = arguments[0];

    // OPTIONAL - The Client Side ID that you want to partial refresh after executing the event handler
    var refreshId = (arguments[1]) ? arguments[1] : "@none";
    var form = (arguments[1]) ? XSP.findForm(arguments[1]) : dojo.query('form')[0];

    // OPTIONAL - Options object contianing onStart, onComplete and onError functions for the call to the
    // handler and subsequent partial refresh
    var options = (arguments[2]) ? arguments[2] : {};

    // Set the ID in $$xspsubmitid of the event handler to execute
    dojo.query('[name="$$xspsubmitid"]')[0].value = form.id + ':' + functionName;
    XSP._partialRefresh("post", form, refreshId, options);

}

Ok, so there is a bit of "magic" in that code as it basically mimics parts of the XSP object to set up the call and execute the event. But the important parts are the arguments you pass to the function. Basically to call the server side event and cause no partial refresh to occur, just call executeOnServer with a single argument of the id of the event handler you want to execute, in our case, we would call it like this:

executeOnServer('eventhandler1a');

If I wanted to partial refresh an object, say for example we had an xp:panel with an id of "refreshTarget", we could refresh that panel after executing the handler by calling executeOnServer like this:

executeOnServer('eventhandler1a', dojo.query('[id$="refreshTarget"]')[0].id);

And finally, if I wanted to execute more code in an onStart, onComplete, or onError event of the partial refresh, I could call executeOnServer like this:

executeOnServer('eventhandler1a', dojo.query('[id$="refreshTarget"]')[0].id, {
    onStart: function() { alert('starting!'); },
    onComplete: function() { alert('complete!'); },
    onError: function() { alert('DANGER WILL ROBINSON!'); }
});

Now, if I wanted to call executeOnServer, and have the capability to do onStart, onComplete, etc, but don't want to actually partially refresh anything, you can use '@none' as the partial Refresh Id, like this:

executeOnServer('eventhandler1a', '@none', {
    onStart: function() { alert('starting!'); },
    onComplete: function() { alert('complete!'); },
    onError: function() { alert('DANGER WILL ROBINSON!'); }
});

As I mentioned before, you can create multiple eventhandlers, and call any one of them at any time. For example, in your XPages markup:

     |xp:view .... |
        |xp:this.data|
             |xp:dominoDocument var="myDocument"
                  formName="myForm" action="editDocument"
                  documentId="#{java_script:viewScope.currentDocId}"|
             |/xp:dominoDocument|
         |/xp:this.data|

         |xp:eventHandler event="onfubar" id="saveDoc" submit="false"|
             |xp:this.action|
                 |xp:saveDocument||/xp:saveDocument|
             |/xp:this.action|
         |/xp:eventHandler|

         |xp:eventHandler event="onfubar" id="deleteDoc" submit="false"|
             |xp:this.action|
                 |xp:deleteDocument||/xp:deleteDocument|
             |/xp:this.action|
         |/xp:eventHandler|

        |!-- everything else goes here --|

    |/xp:view|

And all you'd have to do is call

executeOnServer('saveDoc');

or

executeOnServer('deleteDoc');

to execute the appropriate event handler. The possibilities here are profond, as you now have the ability to very powerfully connect your client side, and your server side event models in a very cohesive way. With a little ingenuity beyond what I have shown here, you can actually pass objects back and forth between the server and client sides, and have both sides operate their respective functions accordingly.

Happy Coding!

Friday
Mar052010

SSJS @Adjust on Javascript Dates - Be Backwarned!

The following code will generate results that you may not expect:

var myDate = new Date(2010,2,15);  //this is MARCH 15th, months for JSDate is zero based
myDate = @Adjust(myDate,0,-1,0,0,0,0);

You might expect myDate to now be 2/15/2010 ... its what the classic @Adjust @Formula would output.  But there is a little more than meets the eye going on here.

new Date(2010,2,15) creates a new Date in the local time zone... In SSJS @Adjust() by default treats the date as a UTC timezone, so the results you will actually get is:

2/14/2010

Why you ask? Because we didnt specify a time in the date object, it's set to 12 Midnight, and going from March back to February crosses the line for Daylight savings, and the adjusted time changes from 12:00:00 midnight to 11:00:00 PM the previous day as you cross back.

The key is to specify the "[InLocalTime]" keyword to the @Adjust function, so the correct call would be:

@Adjust(myDate,0,-1,0,0,0,0,"[InLocalTime]")

Thanks to Dave Connelly and the IBM Team for chasing this one down!

 

Friday
Nov062009

XPage Cheat Sheet #2 - DataSource and NotesXspDocument

In my last blog I did an update on the XPage Lifecycle.  The next blog in this new series will cover some of the basics about DataSources and the new NotesXspDocument object/class.  This is targeted at those people like me making a transition from LotusScript to the new world of XPages and SSJS.

With Notes Classic the data model was quite simple.  Notes databases contained a collection of Notes Documents.  These were represented in LotusScript via the NotesDocument class.  The Form was the design element used to display and maintain these documents.  While it was possible to bring in data from other documents there was usually a very strong one to one relationship between the Form and a Document.  So strong that it was often impossible to separate the two.

DataSources

With XPages we are now starting to see a stronger separation between the data layer and the presentation layer.  Data is now (typically) bound to an XPage using one or more DataSources.  The most common DataSources are the Notes Document and the Notes View.  In the future there are plans to support a much wider range of DataSources including XML, JSON (I assume), and Relational Databases.  (You can do this now using a few tricks).

NotesXspDocument

When using Notes Classic to develop applications for the Notes client most of your code is executed on the client with LotusScript able to maintain a continuous exchange with the server.  With the Web model used by XPages it is either not possible or not practical for the Domino server to keep the NotesDocument objects open for the life of a transaction.  This has given rise to the creation of a new NotesXspDocument object which acts as a buffer between Web Page and the NotesDocument objects holding the data. The NotesDocument object only has a life as long as a single trip to the server.  The NotesXspDocument however stays with the XPage as it travels back and forward between the server and the client.

For each DataSource that is linked to a Notes Document on either an XPage or a Custom Control a global SSJS variable is assigned that matches the name assigned to the DataSource.  This variable is an SSJS object of this new type - NotesXspDocument.  The NotesXspDocument is still tied to a NotesDocument but there are some important differences. 

When creating a Notes Document Data Source you are usually asked to connect to an existing form in this (or another database).  As best as I can tell the form selected is significant for just two purposes:-

  1. It determines the list of fields that will be displayed in the data palette.  This can be useful, but if you know the names of the fields you are working with it is not needed;
  2. When a new document is saved the Form name assigned to the document will be that of the selected form (unless you manually assign a value to the field "Form").

Custom Controls & NotesXspDocuments

If your XPages are designed with CustomControls, care needs to be taken to avoid the possible scenario of a separate NotesXspDocument being created for the XPage and each Custom Control.  I have had all manner of troubles trying to share data between my XPage and Custom Controls until I started to understand what was going on.  e.g. A save action on my Xpage would create five new documents!  If your Custom Controls are designed to maintain data in the same Notes Document then it is usually a good idea to share the one NotesXspDocument between the XPage and these controls. 

  1. John Mackey has previously blogged about Passing the NotesXspDocument as a parameter to a Custom Control.
  2. Another technique (also covered by John) is to use the global variable currrentDocument.  This is a NotesXspDocument object that references the first Notes Document data source assigned to the parent XPage.  On the custom control use Advanced Data Binding and Expression Language to enter currentDocument.fieldname to bind the control to a specific field in the first Notes Document DataSource on the parent XPage.  This may not work if you have multiple DataSources bound to the XPage because it seems to always take the first one listed.  Note: The list of DataSources is maintained in alphabetical order so its not even the first DataSource you create that gets assigned!

Using the above it becomes possible to assign the same field to multiple edit controls either on different Custom Controls or even the same XPage/Custom Control.  If this happens the data entered in the last edit control is the one that is always returned to the NotesXspDocument.  After a refresh the other controls will loose their values and display the value entered in this last edit control.

NotesXspDocument & NotesDocument

It appears that during the initial PageLoad events the existing data is transferred from the NotesDocument to the NotesXspDocument.  Only those fields bound to the NotesXspDocument (on XPage or CC) are transferred.  This does include bound controls that are not rendered (visible).  This same data is then transferred back to the NotesDocument when NotesXspDocument.save() method is invoked.  Alternatively you can manually manipulate the NotesDocument object using SSJS.

Note: If you were to perform a save on a newly created document using the NotesDocument via NotesXspDocument.getDocument().save() you will probably find a document saved with a single field (Form) and none of the data entered on the XPage.

Also note that if you pass the NoteID or UNID to a Custom Control you will bind to the same NotesDocument but most likely create a separate NotesXspDocument.

On-Line Demonstration

To illustrate some of the above I have created an on-line demo in the new XPages Kindergarten section of the .Domino Framework: http://www.dominoframework.com/DominoFramework.nsf/XPK_Events.xsp