The situation: in an ADF Faces 11g page, we have a popup with two buttons: one to start a download and one to cancel the popup. When the users presses the download button, a fileDownloadActionListener is activated, the corresponding server side method is invoked to start producing the content to be downloaded and eventually the browser will prompt the user to open or save (or cancel in the case of IE) the download.
The challenge: when the download commences, the download and cancel buttons should be disabled and perhaps an animated gif should be shown to suggest progress (we want to at least prevent the user from clicking the download button or getting frustrated in other ways while she is waiting for the report to be produced on the server side). When the download is complete – that means: when through the browser interaction the user has saved, opened or canceled the actual content download to the browser – the buttons should be enabled again and the animated gif can be removed.
In short: we want to be able to react – both to the beginning of the download as well as to the completion.
It turns out that responding to the beginning of the download is easy: we can add an af:clientListener component inside the fileDownloadActionListener and have a JavaScript function invoked.
In this JavaScript function, using the ADF 11g Rich Faces Client Side APIs, we can access and manipulate the entire page. However, for security reasons, we can not through JavaScript disable a button. The function setDisabled(true) does not exist. The trick to still ‘suggest’ the disabling and enabling of buttons from JavaScript is to add two additional initially invisble buttons that have the status disabled. The download button has an invisible, disabled counter part and so does the cancel button. When the Download button is pressed, the clientListener fires and sets the visible property for both enabled buttons to false and sets visible to true for the two disabled buttons. At this point, the animated ‘I am in progress’ gif could be shown as well.
Unfortunately, it is not so easy to detect the end of the download. There is no event to capture, neither client side, nor server side.
However, I found out that poll operations during the download are not able to reach the server. So even though the client side timer continues to fire, the server request cycle that could be triggered by a poll event whenever a component is associated with the poll (has the poll as its partial trigger) has an attribute dynamicaly associated through an EL Expression with a server side bean, does not take place. As soon as the download is complete, the next poll event will start reaching the server again.
Here is our clue for a solution: if we can find out that a poll leads to a server based update in the page, that means that the download must be complete because during the download this is not possible.
The approach can now be as follows:
-
create a poll component in the page, set its interval to -1 (which means it does not fire at all)
-
create a simple managed bean in the ADF Faces application with a method Date getLatestValue that returns new java.util.Date().
-
add a component to the page that has an attribute configured with an EL expression that references the bean and its latestValue property
-
add the poll to the partialTriggers attribute of this component
-
add a clientListener to the poll component that calls a JavaScript function processPollEvent
-
add a clientListener to the fileDownloadActionListener that calls a JavaScript function startDownload
-
add an af:resource component of type JavaScript and create a variables startTime and reportRunning. Also create the two JavaScript functions startDownload and processPollEvent
-
in function startDownload, set reportRunning to true and set the variable startTime to the current value of the attribute based on the ‘timebean’ – the latestValue property. Get hold of the poll component and set its interval to 1000 (== 1 second)
-
in function processPollEvent: test whether reportRunning is true. If that is so, then retrieve the value of the attribute based on the ‘timebean’. If that value is not the same as the value stored in startTime – this means that the poll has succeeded in updating the component which means that the download is completed and the channel open again. At this point set reportRunning to false, set the poller interval back to -1 and show the two enabled buttons while hiding the two disabled buttons
Visually outlined, it looks like this:
Note the two parallel event cycles. The white markers show the ‘initiate and execute download’ flow and the yellow markers show the poll-and-refresh-input-component sequence.
also note: JavaScript code is to be added later to this article
Resources
See http://saumoinak.blogspot.com/2011/04/file-download-in-adf.html for a straighforward example of using the fileDownloadActionListener.
Hello, Lucas,
another solution is to use a direct call to put a request to a server into the JS-queue without using a pool.
We can define af:clientListener and af:serverListener for a command button. When a user clicks on a button we call a JS method of af:clientListener (let’s name it as onDownloadStartedJS()). This method is responsible to show ‘Progress Bar’ icon and it also sets JS timeout (500 ms is enough) to put a server-side method (a method of af:serverListener) into the queue (like this AdfCustomEvent.queue(event.getSource(), “completeDownload”, null, false);). Let’s name this method as completeDownloadJS().
Then ADF calls a method which generates output file. At the same time JS-code puts a new request to a server into the queue and this request is suspended automatically until a file gets downloaded. Once it’s downloaded, ADF takes a call from the queue and performs a request to a server.
Here are the steps that are performed by the app:
1) user clicks on ‘download’ button;
2) ADF calls JS-method – onDownloadStartedJS();
3) onDownloadStartedJS method displays a progress bar and sets JS-timeout to call completeDownloadJS() method;
4) onDownloadStartedJS method completed and ADF calls a server-side method (which is defined on af:fileDownloadActionListener) to generate a file. Let’s imagine that we need to wait about 5 seconds to get a generated file;
5) JS-timeout for completeDownloadJS() method finished, completeDownloadJS method is called. This method puts a new server-side request into the queue. But ADF is not able to call this request immediately, because the request from the 4th point is in progress now;
6) about 4.5 seconds later a file is generated and server returns the file to a browser (a user is able to download\open it)
7) ADF is now able to extract a next request from the queue. It takes the request and call a server-side method which is defined in af:serverListener. So, we can catch the moment when a download is completed.
It is not asynchronous as such. Or at least: the pre-processing that takes place on the server is probably asynchronous. However, at least as soon as the download from server to client starts, your browser is not accessible for any other operations. This is the browser’s doing, not something inside ADF.
Â
Lucas
Hi ,
Is not fileDownloadActionListener asynchronous ?
We have used that component for file downloading file but while the download is in progress we can not navigate to other tabs .
Our scenario : we have two adf panel tab . and one tab has the download link . when the download is in progress we can not click on the 2nd tab . It only get clicked after download is completed .
Any input ?