Friday, 14 February 2014

Showing popup in ADF during long running operations


An ADF application can trigger long running operations (DB queries, WS calls, etc), and this could cause user experience issues: user getting impatient, clicking everywhere and (possibly) crashing the application causing unpredictable behaviour (double submitting forms, modifying input data while running).

The following one is a solution proposed from Frank Nimphius in this article of ADF Code Corner: for every long running operation, we show an ADF popup (it's our choice if we want to make it modal or not), making a JS function call via the clientListener component.

The solution is based on 3 steps:

1) Create the popup component, making sure that contentDelivery is set to immediate (to render it when the page loads) and clientComponent is set to true (to make it accessible from JS):

<af:popup id="waitPopup" contentDelivery="immediate" clientComponent="true">
  <af:dialog id="waitDialog" type="none"
             title="#{resBundle.WAIT_POPUP_TITLE}" closeIconVisible="false">
    <af:panelGroupLayout id="pgl2" layout="vertical" halign="center">
     <af:image source="/images/scan.gif" id="i1"/>
     <af:outputText value="#{resBundle.WAIT_POPUP_MSG}" id="waitText"/>
    </af:panelGroupLayout>
  </af:dialog>
</af:popup>

2) Create JS code to disable the user input and open the popup; create a file named waitingpopup.js in a subfolder of public_html folder in your application, and write the following code inside it:
function enforcePreventUserInput(evt) {
    var popup = AdfPage.PAGE.findComponentByAbsoluteId('waitPopup');
    if (popup != null) {
        AdfPage.PAGE.addBusyStateListener(popup, handleBusyState);
        evt.preventUserInput();
    }
}

function handleBusyState(evt) {
    var popup = AdfPage.PAGE.findComponentByAbsoluteId('waitPopup');
    if (popup != null) {
        if (evt.isBusy()) {
            popup.show();
        }
        else if(popup.isPopupVisible()) {
            popup.hide();
            AdfPage.PAGE.removeBusyStateListener(popup, handleBusyState);
        }
    }
} 

After the file is saved, put the resource tag in the page/fragment containing the popup, using the correct path of the file created above (in this case the file has been created under public_html/scripts folder):
<af:resource type="javascript" source="/scripts/waitingpopup.js"/>

3) Trigger the popup by invoking the JS code above via clientListener:

<af:commandLink id="commandLink4" styleClass="btn btn-alt"
    disabled="#{isTaskRunning}"
    actionListener="#{bindings.longOperation.execute}"
    action="back"> 
  <af:clientListener method="enforcePreventUserInput" type="action"/> 
</af:commandLink>


What is not optimal in this solution is the part in which the JS retrieves the popup component by ID: this approach works smoothly if the popup is defined at top level (inside a JSF page or a JSF template: if the popup is defined inside a fragment, the ID for this component at runtime will be prefixed by the id of the region (something like 'r0:waitingPopup'), and in case the ADF taskflow containing this fragment is shown (for example) in a Portal page that is using a template, the component id is prefixed with the ID of the template as well; bottom line is, define this popup always at top level, or the code above won't always work.

No comments:

Post a Comment