Thursday 27 June 2013

Show popup when ADF is busy


There are times when you need to block all user entry when ADF is busy doing a long-running task.
I had this scenario with a long running synchronous call to a web service.

This operation was fired off from a command image link rather than a button

There is a property on the af:commandImageLink, blocking, which is supposed to turn the mouse pointer into an intermediate load state.

When testing this on a development server (i.e. not the JDeveloper integrated Weblogic), I found that it didnt truly block the user input. Also, its not totally clear that the users click was recognised by the app.

Therefore, the best way to do this is to show a popup. This will block all user input and we give a clear indication that the server is doing something.

Step 1) design your 'Loading' popup

You will need to set the contentDelivery to immediate so the popup is injected into the HTML as soon as its rendered out to the user. Speed is of the essence here and the server is busy with our request, so we need this to be pre-loaded.

Its nice to have a loading image too. Generate one from this website, which is free to use:
http://www.ajaxload.info/

<af:popup id="p7" contentDelivery="immediate" >
  <af:dialog id="d3" type="none" title="Loading" closeIconVisible="false">
  <af:panelGroupLayout id="pgl9" layout="scroll" halign="center">
    <af:outputText value="I am loading." id="ot78"/>
    <af:spacer width="10" height="10" id="s2"/>
    <af:image source="/images/ajax-loader.gif" id="i98" shortDesc="loading"/>
    <af:spacer width="10" height="10" id="s3"/>
    <af:outputText value="One moment please..." id="ot11"/>
   </af:panelGroupLayout>
   </af:dialog>
</af:popup>
 
Step 2) show your 'Loading' popup
When  the user clicks the image, I want the popup to show instantly. For this, we use javascript via the clientListener tag:

<af:commandImageLink icon=....
    <af:clientListener method="preventUserInput" type="action"/>
 </af:commandImageLink>

Step 3) javascript to handle the popup

This code handles the finding and showing of the popup as well as creating a busyStateListener which gets called when the loading operation is completed.

Up to this point, everything here can be seen on numerous ADF blogs but they do not discuss how to handle when errors are thrown from your long-running process. This is what the interruptBusyState is for.

Put the following code at the bottom of your JSPX as a resource (or externalise it into a seperate js file if you need it on multiple pages)

  <af:resource type="javascript"> 
     //interrupt handler
     function interruptBusyState(){
     var popup = AdfPage.PAGE.findComponentByAbsoluteId('pt1:p7');
     popup.hide();
     AdfPage.PAGE.removeBusyStateListener(popup, handleBusyState);
   }
       
     //JavaScript call back handler
    function handleBusyState(evt){
    var popup = this;
      if(popup != null){
        if (evt.isBusy()){
          popup.show();
        } else if (popup.isPopupVisible()) {
        popup.hide();
        AdfPage.PAGE.removeBusyStateListener(popup, handleBusyState);
        }
      }
    }
       
    function preventUserInput(evt){
      var popup = AdfPage.PAGE.findComponentByAbsoluteId('pt1:p7');
           
      if (popup != null){
                 AdfPage.PAGE.addBusyStateListener(popup,handleBusyState);
                evt.preventUserInput();
      }
   }
  </af:resource>


So, the preventUserInput method sets up the busy listener with the found popup. When the page is busy, it will run the handleBusyState will get called again when the operation completes and therefore the popup will be hidden.

Notice that we can reference the popup within the handleBusyState method by using this. The this object is created because we called handleBusyState with the popup

Step 4) handle errors in long-running process
If there is an error in your code, you may find that the popup doesnt close itself and that the exception appears behind the popup. This is unacceptable as the user is now stuck with a loading popup and cannot do anything about it.

In my backing bean, I have an operationBinding which executes my long running process. Here is the end of that method, testing to see if errors were raised and interrupting the loading popup if there was one:

if (ob.getErrors().size() == 0){
  return "showMultiplexConfiguration";
} else {
  logger.severe("serious error received in bean; stop the loader");
  interruptLoadingPopup();
}

       ...
       ...

private void interruptLoadingPopup(){
  String script = " interruptBusyState(); ";
  FacesContext fct = FacesContext.getCurrentInstance();
  Service.getRenderKitService(fct,
ExtendedRenderKitService.class).addScript(fct,                                                                  script.toString());



The private method interruptLoadingPopup does the work for us, calling the javascript interruptBusyState() method, as defined in step 3)

Final words
Its not the most fluid of solution because you need to locate the loader popup through its absolute location, but once this is done, its a neat solution that gives the user a full indication of what is going on.

As a side note; you cannot have dynamic content on the loader as its loaded immediately at inital render time. I.e you can have text saying "you clicked on row 3. Loading row 3".

To me, this is just a small caveat.

Happy coding.

No comments:

Post a Comment