17.8. Reporting ProgressIn a perfect world, actions run at the speed of light and the user never has to wait. However, in reality, many actions may take a noticeable amount of time to complete. Without feedback to the user, these actions make your application look unresponsive. Adding progress reporting to actions is therefore critical. The simplest feedback option is to show a busy cursor, as shown in the following: org.eclipsercp.hyperbola/ProgressAction
public void run() {
BusyIndicator.showWhile(display, new Runnable() {
public void run() {
// perform action's work here
}
});
}This is great for operations that are not instant, but are likely to take less than about two seconds. If the action takes longer than two seconds, the busy cursor does not provide enough feedbackit appears that the UI is blocked and there is no possibility of canceling the action. The Runtime's IProgressMonitor interface is useful for reporting progress and allowing cancellation. Below is an example of running a long operation while showing a progress dialog: org.eclipsercp.hyperbola/ProgressAction ProgressMonitorDialog pd = new ProgressMonitorDialog(window.getShell()); pd.run(true /*fork*/, true /*cancelable*/, new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { monitor.beginTask("Long running action", 100); for (int i = 0; i < 10; i++) { if (monitor.isCanceled()) return; monitor.subTask("working on step " + i); Display.asyncExec(new Runnable() { public void run() { textField.setText("New work: " + i); } }); monitor.worked(10); sleep(1000); } monitor.done(); } }); The progress dialog runs the given runnable and displays the main task name, optional subtask names, and a progress bar. A Cancel button allows the user to cancel the action. The ability to cancel an action depends on how often the action checks the IProgressMonitor for cancellation. This example forks the operation. Forking a long-running action is highly desirable because it allows the UI to repaint and process events while the action is runningthe application looks more responsive. It does mean, however, that the action's code must use Display.syncExec(Runnable) or Display.asyncExec (Runnable) to do any drawing since the action is run outside the main UI thread. The problem with progress dialogs is that they flicker and flash when the operation runs quickly. A better solution is to show a busy cursor and only show the progress dialog if the action has been running for more than a specified amount of time. The IProgressService provides this functionality and unifies progress reporting in the Workbench. The progress service tries to achieve a balance between a busy cursor and a dialog. The coding pattern is very similar to that of the progress dialog except that the IRunnableWithProgress.run() method is always forked. The progress service displays a busy cursor first, then pops up a progress dialog when the operation runs for more than a specified threshold. The threshold for switching between a busy cursor and the progress dialog is hard-coded into the Workbench; it's currently 800ms. The following snippet shows how the service is accessed via the IWorkbench: PlatformUI.getWorkbench().getProgressService().
busyCursorWhile(new IRunnableWithProgress() {
public void run(IProgressMonitor monitor) {
// do the work
}
});The progress service also handles the case when a foreground action is blocked by a background action. In this case, the service displays a list of the actions that may be blocking the foreground action and allows them to be canceled by the user. 17.8.1. Non-modal ProgressThe Runtime provides a jobs mechanism that is useful for managing background tasks. The Job class is a cross between a java.lang.Runnable and a java.lang.Thread. Jobs require less overhead than threads because they are pooled. They also support progress and cancellation and can be configured with varying priorities and scheduling rules that describe how they interact with other jobs. Here is a simple Job that does some fake work: org.eclipsercp.hyperbola/ProgressAction
Job job = new Job("long running action") {
protected IStatus run(IProgressMonitor monitor) {
monitor.beginTask("Long running action", 100);
// do some work
return Status.OK_STATUS;
}
};
job.setUser(true);
job.schedule();As with actions, the user probably wants to know about running jobs, but not in a way that interrupts or blocks the UI. Not all jobs are alike and the progress presentation depends on how the job is created. There are three categories of jobs:
The Workbench shows progress for user jobs using an area in the status line, as shown in Figure 17-4, and supplies a modal dialog that contains a Run in background button. The background job progress area has to be explicitly enabled in WorkbenchWindowAdvisor.preWindowOpen() as follows: configurer.setShowProgressIndicator(true); Figure 17-4. Showing job progress in the status line
When this is enabled and a background job is run, the user is presented with a progress dialog. If the user selects Run in background, the dialog is dismissed and progress is shown in the progress area in the status line. Note The sample code for this chapter includes an example action that runs several long-running actions using the different progress feedback mechanisms. The last action is run as a job. The action can be found in the main menu as Tools > Long running action example. 17.8.2. Progress ViewThe Workbench also provides a view that displays progress for multiple jobs simultaneously. It complements the status line progress by providing more detailed status information about running jobs as well as the ability to cancel jobs. In everyday usage, users are not expected to use the progress view since background jobs should complete reasonably quickly and users can keep working while background jobs run. Nevertheless, if you want to include the progress view in your application, add the following view extension to your plugin.xml file: org.eclipsercp.hyperbola/plugin.xml <extension point="org.eclipse.ui.views"> <view class="org.eclipse.ui.ExtensionFactory:progressView" icon="icons/progress.gif" id="org.eclipsercp.hyperbola.views.progress" name="Progress"/> </extension> You can give the view any name and still use the implementation provided by the Workbench's ExtensionFactory. Don't forget to update your perspective to show the progress view, as shown below: org.eclipsercp.hyperbola/Perspective public class Perspective implements IPerspectiveFactory { public void createInitialLayout(IPageLayout layout) { layout.addStandaloneView(ContactsView.ID, false, IPageLayout.LEFT, 0.33f, layout.getEditorArea()); layout.addView("org.eclipsercp.hyperbola.views.progress", IPageLayout.BOTTOM, 0.22f, layout.getEditorArea()); } } You can control a job's icon and various other job properties using the constants in IProgressConstants and the Job method setProperty(QualifiedName, Object). 17.8.3. Customizing ProgressJob progress support in Eclipse is tailored for large-scale IDE applications, in which the type of background jobs are not known at design time. A quick survey of existing non-Eclipse-based products confirms that there are no standard ways of showing background progressthe chosen solution is often domain-specific and highly integrated. Here is a list of different ways applications can show background progress:
If you decide that you don't like the background progress support in Eclipse, it's possible to implement a custom solution. To demonstrate, let's work through an example of replacing the existing support with an implementation that shows progress for background jobs in separate non-modal dialogs. When the background job is finished, the dialog is automatically closed. Jobs are created by the user and run by the Platform. The IProgressMonitor passed to the job's run() method is used to report the job's progress. Here is an example of a job reporting progress: Job job = new Job("Working") {
protected IStatus run(IProgressMonitor monitor) {
monitor.beginTask("Calculating things", 1000);
for (int i = 0; i < 1000; i++) {
monitor.worked(1);
monitor.subTask("doing " + i);
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
}
monitor.done();
return Status.OK_STATUS;
}
};
job.setUser(true);
job.schedule();To write your own progress reporting, first implement a subclass of org.eclipse.core.runtime.jobs.ProgressProvider and then register the provider with the Workbench. The provider is responsible for creating IProgressMonitor instances for use by the job framework when running jobs. This, combined with notifications about the lifecycle of jobs, allows you to implement background progress reporting. The Workbench registers its progress provider only if progress support is enabled; otherwise, you can register your own in the WorkbenchAdvisor method preStartup(). Platform.getJobManager().setProgressProvider(new DialogProgressProvider()); 17.8.4. Writing a ProgressProviderThe most interesting method on the ProgressProvider is createMonitor(Job). This method returns the progress monitor that is passed to the running job. In our example below, a non-modal progress monitor dialog is created every time a new job is run: org.eclipsercp.hyperbola/DialogProgressProvider public class DialogProgressProvider extends ProgressProvider { public IProgressMonitor createMonitor(final Job job) { final IProgressMonitor[] m = new IProgressMonitor[]{ new NullProgressMonitor()}; Display.getDefault().syncExec(new Runnable() { public void run() { final ProgressMonitorDialog dialog = new NonBlockingProgressMonitorDialog( Display.getDefault().getActiveShell()); dialog.setBlockOnOpen(false); dialog.setCancelable(true); dialog.open(); job.addJobChangeListener(new JobChangeAdapter() { public void done(IJobChangeEvent event) { // what if the job returned an error? close(dialog); } }); m[0] = new AccumulatingProgressMonitor( dialog.getProgressMonitor(), Display.getDefault()); } }); return m[0]; } private void close(final Dialog d) { Display.getDefault().asyncExec(new Runnable() { public void run() { if (d != null && ! d.getShell().isDisposed()) d.close(); } }); } } The essential part of this snippet is the subclass of ProgressMonitorDialog that makes the progress dialog non-modal. The new dialog is used to create an IProgressMonitor that shows progress using an indicator with status messages and task names. When a job completes, the dialog is closed. To keep this example simple, the error status of the job is not checkedit is possible to check the return status and prompt the user or provide an indication that an error has occurred. The dialog's progress monitor is wrapped with another progress monitor, AccumulatingProgressMonitor, that ensures all calls to the IProgressMonitor methods by the background job are run in the UI thread. This is important because the dialog assumes that it is being called from the UI thread, whereas jobs are always run in a non-UI thread. Warning The progress view relies on the Workbench's progress provider. If you add your own provider, the progress view stops working as there can only be one progress provider registered at a time. |