• Share this article:

DeferredContentProvider: My new favourite thing

Wednesday, January 16, 2008 - 22:46 by Wayne Beaton

I’ve built two tables over the past two days and both of them are pretty expensive to populate. Leaving the user waiting while you run off and fetch all the data before you can display the table is bad form. To keep in your users’ good graces, consider using the DeferredContentProvider.

DeferredContentProvider is part of JFace, and works well with the JFace TableViewer (I assume that it can also work with a TreeViewer, but haven’t tried it yet). Rather than try and do a bad job at explaining how it works, here’s an example of an Eclipse view that employs one:

...
public class SampleView extends ViewPart {
  private TableViewer viewer;
  private SetModel files = new SetModel();
  private Job findFilesJob;
  ...
  public void createPartControl(Composite parent) {
    viewer = new TableViewer(parent, SWT.VIRTUAL | SWT.H_SCROLL | SWT.V_SCROLL);
    viewer.setContentProvider(new DeferredContentProvider(new Comparator() {
      public int compare(File file1, File file2) {
        return file1.getName().compareTo(file2.getName());
      }
    }));
    viewer.setLabelProvider(new ViewLabelProvider());
    viewer.setInput(files);

    startFindFilesJob();
  }
  ...
}

This view lists—in a table—all the files in your file system. As you might expect, this can take a while. Starting from the top of the createPartControl method, you’ll notice that I’ve created the TableViewer using the SWT.VIRTUAL style: this is a requirement of use for the DeferredContentProvider (it configures the table to ask for the bits it needs to display as it needs them, rather than getting them all up front; more information here).

The content provider for the TableViewer is set to a new instance of DeferredContentProvider. The solitary constructor requires that a Comparator be provided: this Comparator, which must be provided, tells the content provider how to sort the items in the table. If you want to change how items are sorted, you need to tell the content provider, not the TableViewer.

The next bit is the input. The input tells the table what to display. The input has to be something that implements IConcurrentModel. For our purposes, an instance of the SetModel class (which has been created in the field named “files”) does what we need.

The last thing the method does is start a Job to populate the table via the startFindFilesJob method:

private void startFindFilesJob() {
  findFilesJob = new Job("Find Files") {
    @Override
    protected IStatus run(IProgressMonitor monitor) {
      monitor.beginTask("Find files", IProgressMonitor.UNKNOWN);
      for (File root : File.listRoots()) {
        findFiles(monitor, root);
      }
      if (monitor.isCanceled()) return Status.CANCEL_STATUS;
      return Status.OK_STATUS;
    }
  };
  findFilesJob.setPriority(Job.DECORATE);
  findFilesJob.schedule();
}

Note that this very simple example starts populating itself when it is opened, and I haven’t provided any means of restarting the population process other than to close and reopen the window.

Creating the job is pretty straightforward. I’ve decided to set the priority of the job to the lowest setting (Job.DECORATE) because experience has shown that it takes a very long time and I want the rest of the workbench to be as responsive as possible. The job, as defined above, will appear in the Progress view where is can easily be canceled. There’s more information about the Jobs API here.

The findFiles method recursively discovers the files and adds them to the SetModel (highlighted):

void findFiles(IProgressMonitor monitor, File root) {
  if (monitor.isCanceled()) return;
  File[] children = root.listFiles();
  if (children == null) return;
  files.addAll(children);
  for (File file : children) {
    findFiles(monitor, file);
  }
}

When the addAll method is called, the magic happens and the table is updated. It’s pretty neat to watch. Hypnotic even.

For completeness, the dispose method stops the job (no need to keep doing the work if the user closes the view):

@Override
public void dispose() {
  findFilesJob.cancel();
}

For what I’m doing, the SetModel seems to work well. I believe that this is true only because I’m updating it in a single thread. A quick browse through the code leads me to believe that it would break if multiple threads tried updating it at the same time. So keep that in mind.

I’m using the DeferredContentProvider on a plug-in that scans an IFileStore for image files and displays them (right now, I have it scanning eclipse.org’s CVS server). The deferred loading of the images is darned cool to watch. Once this reaches a reasonable level of maturity, I’ll let you know.