Visual Notification During Long Running Transactions

From time to time, there will be transactions within your application which do not finish within the "blink of an eye". Later this week I'll be writing up a series of articles specifically on launching long running tasks asynchronously, but first I wanted to cover the case of a transaction which has to happen in real-time but ties up the browser whilst it's doing so. 

A file upload is a typical example of this. The user selects a 500MiB file and presses submit, it's probably time to go and get a cup of tea.  So the question is, how do you tell the user to go and find something more interesting to do for a while and, for that matter, please don't close your browser window...

Both Frank Nimphius and Andrejus Baranovskis have written articles in the past  (How-to show a glasspane and splash screen for long running queries and ,Glasspane in ADF Faces RC, respectively). However, both of those articles concentrated on showing a dialog as a glasspane to block user input and notifiy the user of some information.  In my case the upload was already in a dialog, so popping up another layered dialog on top of that would be ugly, so I wanted to find a way to display a loading indicator of some sort, in-line. 

The result is a JavaScript routine that can be called from  a clientListener which will either pop the glasspane and dialog if you point it to a dialog, otherwise it will display an inline component, such as an image with a spinning logo or some text.

The Script

 Here is the JavaScript, the first method showWhenBusy(), is the one called from the clientListener. It reads the ID of the component that we want to show/hide from an clientAttribute called loadingIndicatorId. This makes the code nice and generic as we've not had to hardcode component IDs. 

//Global variable to hold the component ref.
var loadingIndicatorComponent; 

function showWhenBusy(event) {
  //get the dialog or other component we want to show and hide
  var componentId = event.getSource().getProperty('loadingIndicatorId');
  loadingIndicatorComponent = AdfPage.PAGE.findComponent(componentId);
    
  if (loadingIndicatorComponent != null) {
    AdfPage.PAGE.addBusyStateListener(loadingIndicatorComponent,handleBusyStateCallback);        
    event.preventUserInput();
  }
  else {
    AdfLogger.LOGGER.logMessage(AdfLogger.SEVERE, "Requested indicator compoenent not found");
  }
}

As you can see, all that this method does is to store the indicator component into a global JS variable and then create a busy state listener that the framework will invoke as it starts and ends the blocking operation.

The Listener

The listener is where all of the work happens. here we first of all check to see if the requested indicator component is a dialog or not, and then based on the busy state we do the right thing to show or hide.  In the case of the dialog this is a simple matter of calling the show() and hide() methods on the component.  In the case of any other component we achieve the effect by setting the CSS display style.  Note that in order to do this, we need to get a handle to the real DOM element that represents this component.  This is what the call to AdfAgent.AGENT.getElementById() call is doing:

function handleBusyStateCallback(event){
        
  if(loadingIndicatorComponent != null){
    // Check is this is a dialog as 
    // this needs different treatment
    var isDialog =
        (loadingIndicatorComponent.getComponentType() == "oracle.adf.RichPopup");

  if (event.isBusy()){     if (isDialog){       loadingIndicatorComponent.show();        }       else {       loadingIndicatorComponentId = AdfAgent.AGENT.getElementById(loadingIndicatorComponent.getClientId());         loadingIndicatorComponentId.style.display = "inherit";       }     }     else {     if (isDialog){       loadingIndicatorComponent.hide();       }       else {       loadingIndicatorComponentId = AdfAgent.AGENT.getElementById(loadingIndicatorComponent.getClientId());         loadingIndicatorComponentId.style.display = "none";       }

      AdfPage.PAGE.removeBusyStateListener(loadingIndicatorComponent, handleBusyState);     }  }    }

Wiring it up 

In order to call the script here we need to have a reference to it in the page. The normal place would be using an <af:resource> tag in the metaContainer facet of the document:

<af:document>
  ...
  <f:facet name="metaContainer">
    <af:resource type="javascript" source="/js/longRunningNotification.js"/>
  </f:facet>
</af:document>

Then the triggering component itself  uses a client listener to wire up the action and a clientAttribute to pass in the value of the required indicator component:

<af:commandButton text="Start Fong File Upload with Inline Message"
                  id="cb_upload_i"
                  partialSubmit="true">
  <af:clientAttribute name="loadingIndicatorId"
                      value="#{requestScope.uploadBB.loadingIndicatorId}"/>
  <af:clientListener method="showWhenBusy"
                     type="action"/>
</af:commandButton> 

Notice that in this case, rather than passing a hardcoded ID through to the clientAttribute I'm calling a  backing bean getter (#{requestScope.uploadBB.loadingIndicatorId}). The idea of this is that we can ask the component itself for it's correct ID, reducing the margin for error. I have to give Frank the credit for this, it was his idea as we discussed this issue.

Set Up the Indicator Component 

For this to work, the component that I'm using as the indicator  needs a few attributes set:

  1. rendered and visible must be true
  2. clientComponent must be true 
  3. bindings must be set to associate the component with a reference in a backing bean
  4. If the component is not a dialog then we need to set it's initial display state to none so it will not be visible. This is done with inlineStyle.   

Here's a sample of a panelBox that we might use as the "busy" indicator: 

<af:panelBox text="Uploading your large file...." id="pb1"
             clientComponent="true"
             binding="#{uploadBB.loadingBox}"
             inlineStyle="display:none;">
 <af:panelGroupLayout id="pgl5" layout="horizontal">
    <af:spacer width="60" height="10" id="s1"/>
    <af:image source="/images/working.gif" id="i1"/>
 </af:panelGroupLayout>
</af:panelBox

Finally Wiring the ID 

The only missing bit now is how we get the ID of the component above into the clientAttribute that the JavaScript method is pulling. Recall that this was bound to the expression "uploadBB.loadingIndicatorId". So here's the implementation of that getter that lives in the page backing bean:

public String getLoadingIndicatorId() {
  return getLoadingBox().getClientId(FacesContext.getCurrentInstance());
} 

I think that this nicely extends Frank's technique to open up a whole new range of UI possibilities when you're doing something that is going to take some time and want to keep the user entertained. 

Comments:

Good Article. I was wondering if you could comment on this technique for canceling a long running query before it finishes.
http://dkleppinger.blogspot.com/2012/01/how-to-cancel-long-running-query-from.html
Specifically if you think this his something that could cause threading problems or memory leaks
Thanks

Posted by Don Kleppinger on January 25, 2012 at 10:17 PM GMT #

Don, in reference to your post, sad to say this is a little dodgy as a technique. The ViewObject Instance is not serializable so you may run into trouble with this in a cluster. It also feels wrong to be reaching out to the UI layer session from within the Service layer.
That being said I tried to do this the "correct" way last night, where I created a servlet that correctly grabs the binding context and then issues the cancel from an AJAX call kicked off from JavaScript. However, that does not work, perhaps not surprisingly because the binding filter ensures only single threaded access. In a fit of enthusiasm I also created a separate thread in the service layer to see if we could cancel via a JMS message or similar but this was blocked to.
Maybe you should stick with the timeout mechanism?

Posted by Duncan on January 26, 2012 at 03:52 PM GMT #

Thanks for your response! That's too bad there isn't something built in to do this. Sometimes in the search interface we click the search button and then realize that we should have added some different criteria but have to wait a long time before the results come back to try again. It would be real nice to be able to cancel it.

I'm guessing that when ADF times out a query it calls the same cancelQuery method from another thread so I think it would at least be thread safe. Maybe some sort of JMX mbean interface could be created that would route the cancel request to the correct server to get around the Clustering problem. At any rate I think it's a valid use case looking for a solution.

Thanks again.

Posted by Don on January 26, 2012 at 06:01 PM GMT #

Good article!
However, would it be a more recommended ADF approach using an af:progressIndicator component (maybe even together with an af:poll to show percentage progress)?
I used this approach in a project and I know it works.

Posted by Ciprian Iamandi on January 27, 2012 at 01:05 PM GMT #

In the case of file upload then I don't see how you could have a meaningful progress bar for two reasons.
1) The upload is blocking so you'd have to use a javascript timer to update the bar on the client
2) You don't know ahead of time how large the file is and how long it will take to upload.
For other log running transactions where you can make callbacks to the client via a poll and you do know how long it will take then sure a progressBar is the thing to use. In this case though its no good apart from in it's spinning mode as a visual indicator that something is going on.

Posted by Duncan on January 27, 2012 at 03:59 PM GMT #

Thanks for your response! That's too bad there isn't something built in to do this. Sometimes in the search interface we click the search button.

Posted by nike chaussures on January 29, 2012 at 12:50 PM GMT #

I need to do some validation in the actionlistener of the button (cb_upload_i) in a managed bean and need to show the indicator popup only if it passes the validation.

How do I can call showWhenBusy javascript method after the validation in the bean?
Will I need to use ExtendedRenderKitService?
http://myadfnotebook.blogspot.com/2010/11/executing-javascript-in-adf-rc-page.html
I am wondering if the same scripts will be added multiple times when the button is clicked multiple times if ExtendedRenderKitService is used.

Posted by HY on February 15, 2012 at 07:50 PM GMT #

Yes you should launch the javascript via the ExtendedRenderKitService. You can block multiple invocations by using the glasspane techniques pointed to earlier in the article.
Duncan

Posted by Duncan on February 16, 2012 at 10:08 AM GMT #

Post a Comment:
Comments are closed for this entry.
About

Hawaii, Yes! Duncan has been around Oracle technology way too long but occasionally has interesting things to say. He works in the Development Tools Division at Oracle, but you guessed that right? In his spare time he contributes to the Hudson CI Server Project at Eclipse
Follow DuncanMills on Twitter

Note that comments on this blog are moderated so (1) There may be a delay before it gets published (2) I reserve the right to ignore silly questions and comment spam is not tolerated - it gets deleted so don't even bother, we all have better things to do with our lives.
However, don't be put off, I want to hear what you have to say!

Search

Archives
« April 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today