Show dynamic progress of time-consuming process in Seam/RichFaces
Posted by Andrey Chorniy on October 22, 2010
Sometimes you need to start really time-consuming task on the server, but doing that with simple h:commandAction which will re-render page after it is done is not a good idea. Showing a4j:status (which could be blocking – see “avoid concurrent call to conversation“) – is a bit better and will work for relatively short tasks (like 5 – 15 seconds, matter of taste actually) but still not good solution for really long tasks (something more than 1 minute)
For really long tasks I recommend to show the progress indicator (it’s also known usability fact that user will think that task with dynamic progress-bar is faster than the same task but without progress-bar)
There is a quite good component to do it with Richfaces – it’s progressBar component, the only problem to do it with Seam is that you should initiate progress (by calling the action) and that action should immediately return and start some background process. It’s always bad practice to use Thread’s in the web-containers, in Seam we have alternative Asynchronous tasks, but.. the problem with them is that they actually will be running in completely separate scope (they don’t know nothing about your conversation scope)
The trick here is to pass all the required parameters to asynchronous method and use them for returning the results too (in the conversation). That way we are not crazy about memory issues, since as long the long computation process will be ended the memory will be released (the reference to the object will be only in the initiator – the conversation scoped bean)
So, the solution may look like that.
- Define the PrgressBean.java which will hold all required initialization parameters for the process and methods to access the progress-state
- add “progressBean” attribute (+getter) in your action-class (ConversationBean.java)
- define methods to start process which will make a call to asynchronous method in other “LongProcess.java” and pass the progress bean to it
- add rich:progressBar and start/stop buttons in “commandPanel” (could be done inside progressBar only), + “updatedResults” – here you can show intermediate and final results during the process (optional)
In short – it is all you need to show very informative long-running process progress.
Future Notes:
Richfaces has few other ways to build your own progressBean – it is a4j:poll and a4j:push components. rich:progressBean actually utilize “pooling” approach, in most cases it’s quite enough to periodically update progress/results for the user, so actually there is no much sense to write your own approach using a4j:poll. a4j:push maybe quite good alternative to rich:progressBar since it use much less traffic and doesn’t update JSF tree (so it’s potentially a better alternative). I think you can easily adapt described approach to use a4j:push – you just need to add few pieces (addListener method and send events to the listener during the process)
Code-Snippets
Java Code:
@Name("longProcess")
@AutoCreate
public class LongProcess {
private ProgressBean progress
@Asynchronous
public void startProcess(ProgressBean progress) {
this.progress = progress;
this.progress.setInProgress(true);
try {
runProcess();
} finally {
this.progress.setInProgress(false);
}
}
private void runProcess(){
//perform your time-consuming operations here and periodically update the progress status
....
progress.setPprogress(progressValue);
....
if (progress.shouldStop()){
//finish long process and return
}
....
}
}
@Name("conversationBean")
@Scope(ScopeType.CONVERSATION)
public class ConversationBean {
private ProgressBean progressBean = new ProgressBean();
@In LongProcess longProcess;
public void startProcess(){
if (!progressBean.isInProgress()) {
progressBean = createNewProgressBean(); //initialize it with required parameters here
longProcess.startProcess(progressBean);
}
}
public ProgressBean getProgressBean(){
return progressBean;
}
public void stopProcess(){
progressBean.stop(); //update the internal state of progress-bean so long-process will check it and stop
}
}
Richfaces/JSF code
<h:panelGroup id="commandPanel">
<a4j:commandButton action="#{conversationBean.startProcess}"
value="Start" onclick="this.disabled=true;"
rendered="#{!conversationBean.progressBean.inProgress}"
reRender="commandPanel,proggressPanel,updatedResults">
</a4j:commandButton>
<a4j:commandButton action="#{conversationBean.stopProcess}"
value="Stop" onclick="this.disabled=true;"
reRender="commandPanel,proggressPanel,updatedResults"
rendered="#{suggestedEventController.progressBean.inProgress}">
</a4j:commandButton>
</h:panelGroup>
<a4j:outputPanel id="proggressPanel">
<rich:progressBar value="#{conversationBean.progressBean.progress}"
label="#{conversationBean.progressBean.progress} %"
enabled="#{conversationBean.progressBean.inProgress}"
minValue="-1" maxValue="100"
interval="#{conversationBean.updateRate}"
reRender="#{conversationBean.shouldUpdateTable ? 'updatedResults':'anyEmptyComponent'}"
reRenderAfterComplete="proggressPanel, updatedResults, commandPanel">
<f:facet name="initial">
<h:outputText value="&lt; Click to start"/>
<!-- we also may show here button to start process as in RichFaces example (I use separate commandPanel instead) -->
</f:facet>
<f:facet name="complete">
<h:outputText value="Process Completed"/>
<!-- we also may show here button to restart process as in RichFaces example -->
</f:facet>
</rich:progressBar>
<h:panelGroup id="updatedResults">
<!-- it could be used to show results (for example list of processed or generated rows) -->
<rich:dataTable value="#{conversationBean.progressBean.items}>
</rich:dataTable>
</h:panelGroup>
</a4j:outputPanel>
Please take a notice at 2 parameters used in rich:progressBar
interval="#{conversationBean.updateRate}"
it use a method conversationBean.updateRate to determine the update rate. I’s optional and could be just hardcoded to some value like “1000″ (1 second). Can be used to dynamically set it to appropriate value, so your progress bar will not be updated to often and can be even changed during a process to fit to your real update rate
reRender="#{conversationBean.shouldUpdateTable ? 'updatedResults':'anyEmptyComponent'}"
As you see reRender here use dynamic condition, so conversationBean has a control other it and can skip heavy “updatedResults” update to save a traffic
Andrey Chorniy said
code snippets updated (missed JSF piece has been added)
Javier Rodríguez said
Thanks Andrey for this post. I have used a little modified version of your code in one of my projects and it was very usefull and works fine.
Andrey Chorniy said
You welcome
Ann said
This post is really useful.. Thanks a lot for putting it together
Time consuming processes with Seam, a4j:poll and Quartz | Forkbomb Blog said
[...] tasks with Seam. I must admit, that I was heavily(!) inspired by Andrey Chorniys blog post Show dynamic progress of time-consuming process in Seam/RichFaces where I shamelessly stole code and adapted it to my [...]
Dominik Obermaier said
Thank you for this wonderful post. I have written another post where I shamelessly stole some of your code and did the same with a4j:poll: http://bit.ly/qsvbyR
Keep up writing this high quality blog posts!