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.