Tuesday Jan 24, 2012

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. 

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