Thursday, September 29, 2011

<af:query> loading screen / spinner pop-up v2.0

This is an update/follow up on a previous post about displaying a spinner or loading screen while a query is in progress. The original post can be found here. This post describes a much simpler method for getting the same result, and addresses a flaw in the original approach. Its a direct adaptation of the method described by Frank Nimphius in the ADF Code corner example.

The general idea here is to

  1. Execute a custom piece of JavaScript code when user hits 'Search' in af:query
  2. In our JS code, add a busy state listener to fire when the busy state changes.
  3. Show hide a popup based on the busy state.

The ADF code corner example is based on a normal command button. But in the case of af:query we don't have access to the command button that fires the search. In this case, we can use a Client Listener on the af:query that fires on the "query" event.  like below :


       



The example above, the JavaScript function 'fireLoadingScreen' is a custom JavaScript function and  is implemented as below :

          function fireLoadingScreen(evt){
            var source = evt.getSource();
            var popup = AdfPage.PAGE.findComponentByAbsoluteId('spinnerPopUp');
            if(popup != null ){
              AdfPage.PAGE.addBusyStateListener(popup,handleBusyState);        
              evt.preventUserInput();
            }
          }
          
          function handleBusyState(evt){
            var popup = AdfPage.PAGE.findComponentByAbsoluteId('spinnerPopUp');        
            if(popup!=null){
              if (evt.isBusy()){
                popup.show();   
              }
              else if (popup.isPopupVisible()) {
                popup.hide();
                AdfPage.PAGE.removeBusyStateListener(popup,handleBusyState);
              }
            }
          }



The complete example can be downloaded from here : http://myadfnotebook.googlecode.com/files/LoadingScreen2.0.zip

One word of caution. There are implications to using AdfPage.PAGE.findComponent in conjunction with JSF fragments. One way of working around the limitation described in this previous post is to use  

function fireLoadingScreen(evt){
        var source = event.getSource();
        var popup = source.findComponent('spinnerPopUp');
        if(popup != null ){
            popup.show();
        }
        

This uses the source UIComponent object to search for the popup component (not from the root of the document). So the search is relative to the af:query. But now you'd have no way to close the popup but to use the old backing bean based method though.

For those interested, lets look at what was actually wrong with the original approach ( other than being very convoluted )...
While the original post still holds good for a lot of situations (especially if you use jsff and are deploying the app as a portlet) , when specifically applied to af:query, it has one drawback that I recently came to know about. This drawback will be apparent only in a load balanced and highly available environment. If you use a load balancer or fail-over system, there is a chance that the user's session might be migrated to another server while a query is happening. When that happens, things go bad if you use the approach detailed in the older post . Lets see why.


Flow of events for the original solution


The original approach can be applied generically to normal command components or af:query (though there are other more declarative ways of doing this with command components). This method is useful if upon clicking a command component if you want to execute some logic in your backing bean to choose a specific pop-up/loading screen (our customers wanted different types of loading screens for various types of input specified by the user in the af:query ). What makes the implementation different for af:query vs. a normal command button is that in af:query you don't have direct access to the search button (unlike a normal command button). So we had to intercept the search action and hook in to it (step 1). The idea was to fire a user-input blocking pop-up by hooking in to the search action (but not start the search yet) (step 2), then fire the actual search from the pop-up once its visible (steps 3,4,5). In doing that we had to save the QueryEvent as a member variable in the backing bean. The query event would be provided to the method registered as the query listener (step 1), but since we don't actually do the query from that method(we just launch the popup from here) and fire the actual query from the pop-up using a second method(step 6), we need to make the QueryEvent object available to that second method (in step 6). And this is not a simple case of calling one method and passing a parameter to it. The second method is invoked using java script code to call back the backing bean. So we had to store the QueryEvent object and guess what, it is not Serializable ! So in the event a session is replicated, the serialization of the backing bean would fail. At this point one might think we can just make the QueryEvent transient and get around the serialization; But that door opens to a far more subtle and dangerous problem. Suppose we mark it transient, and there are a thousands of users on our load balanced system executing searches. The load balancer decides to move some sessions across to another server, and lets say it just happens to pick a session where a user just clicked the search button, and we've just saved the QueryEvent and opened our loading screen popup (step 1 complete). The entire session is serialized and replicated on the second server, and when the session is inflated on the second server, since we marked the QueryEvent as transient , the QueryEvent is set to null on the second server. Now things resume, but when we reach step 6 in our flow, instead of seeing a QueryEvent we would see null. What makes this problem really frustrating is that since the load balancer is involved, the issue will not be easily reproducible and it would be very difficult to trace.

As you can see, the new solution makes the whole process a lot simpler actually. As long as you have a single loading screen you want to display whenever the search button is hit, this solution is much better that the original one. 

6 comments: