Deferred Fetching of Model Elements with JFace Viewers
Model elements displayed by Eclipse JFace Viewers sometimes take a considerable amount of time to load. Because of this the workbench provides the type IDeferredWorkbenchAdapter
to fetch such model elements in background. Unfortunately this mechanism seems to be supported only for AbstractTreeViewer
derivates via the DeferredTreeContentManager
.
Hence I developed a generic DeferredContentManager
of my own… It enables background loading for all StructuredViewer
types that allow to add and remove model elements. And in this post I explain how it works and how it can be used.
In the need for (re-)use of background fetching with a TableViewer
, I solely found an old and unresolved platform bug regarding this topic. But I doubt that issue’s proposed solution of implementing an additional content manager for table viewers would be very smart anyway. So I decided to give a selfmade generic solution that is based on the concepts of the available tree specific implementation a try.
Deferred Fetching of Content with JFace Viewers
The basic principle of dealing with long loading model elements in JFace Viewers is simple. Rather than fetching the content within IContentProvider#getElements(Object)
directly, data retrieval is delegated to a particular adapter that performs it in a background job.
Moreover, the delegating getElements(Object)
implementation returns a place holder. This is shown by the viewer as long as data loading takes place. In the meanwhile collected data gets forwarded to an update job. The latter appends the elements to the structured viewer. The update job is a derivate of UIJob
since SWT widget access is only allowed from code executed by the UI Thread.
Finally when background fetching has been completed a cleanup job removes the placeholder.
Deferred fetching of content should not be confused with lazy loading of elements using the SWT.VIRTUAL
flag. While there are similarities between both approaches, virtual table and trees are generally useful for partial on-demand loading of large datasets.
Deferred loading is helpful for reasonable sized datasets, which nevertheless might be time consuming to retrieve and therefore would block the UI thread. Consider fetching of remote data for example. And in case you wonder, both approaches are of course mutally exclusive…
IDeferredWorkbenchAdapter
From the developer’s point of view the IDeferredWorkbenchAdapter
is the way to go. It is an extension of IWorkbenchAdapter
, which in general is responsible to ‘provide visual presentation and hierarchical structure for workbench elements, allowing them to be displayed in the UI without having to know the concrete type of the element’ – as stated by its javadoc.
The extension declares additional methods to support deferred fetching of children of a given data element and can be registered by an adapter factory. Consider a simple pojo that serves as model element for example:
public class ModelElement { [...] }
In order to abstract visual presentation and background loading from the domain classes provide an appropriate adapter implementation…
public class ModelElementAdapter implements IDeferredWorkbenchAdapter { [...] }
… and map both types together using an adapter factory:
public class ModelElementAdapterFactory implements IAdapterFactory { @Override public Object getAdapter( Object adaptableObject, Class adapterType ) { return new ModelElementAdapter(); } @Override public Class[] getAdapterList() { return new Class[] { ModelElement.class }; } }
For more information about using IAdaptable
, IWorkbenchAdapter
and IAdaptableFactory
you might have a look at How do I use IAdaptable and IAdapterFactory?. Sadly the default workbench content and label providers expects the model elements to implement IAdaptable
. However this can be circumvented by using custom providers.
The following test sketch verifies that element adaption works as expected:
@Test public void testAdapterRegistration() { IAdapterManager manager = Platform.getAdapterManager(); ModelElementAdapterFactory factory = new ModelElementAdapterFactory(); manager.registerAdapters( factory, ModelElement.class ); Object actual = manager.getAdapter( new ModelElement(), ModelElement.class ); assertThat( actual ) .isInstanceOf( ModelElementAdapter.class ); }
Now it is about time to implement the data retrieval functionality of the ModelElementAdapter
. This is done in the fetchDeferredChildren
method:
@Override public void fetchDeferredChildren( Object parent, IElementCollector collector, IProgressMonitor monitor ) { collector.add( loadData( parent ), monitor ); } private Object[] loadData( Object parent ) { return [...] }
Time consuming data loading is obviously handled by the method loadData()
. Adding the data elements to the IElementCollector
triggers the update job mentioned above. As you can see data fetching could be devided in several steps and progress could be reported via the given IProgressMonitor
.
DeferredContentManager
The last thing to do is to connect the mechanism described in this post with the viewer instance used to depict the model elements. For this purpose DeferredContentManager
can adapt arbitrary viewers and delegates element retrieval to the appropriate IDeferredWorkbenchAdapter
implementation.
class ModelElementContentProvider implements IStructuredContentProvider { DeferredContentManager manager; @Override public void inputChanged( Viewer viewer, Object oldInput, Object newInput ) { TableViewerAdapter adapter = new TableViewerAdapter( ( TableViewer )viewer ); manager = new DeferredContentManager( adapter ); } @Override public Object[] getElements( Object inputElement ) { return manager.getChildren( inputElement ); } [...] }
A custom IStructuredContentProvider
is used to adapt the viewer in its inputChanged
method. The implementation of getElements
delegates to the content manager, which in turn delegates element loading to the model element adapter using DeferredContentManager#getChildren
.
While fetching proceeds, a placeholder element is returned to show a ‘Pending…’ label in the viewer. This is the situation shown in the title image on the left hand side. On the right side retrieval has been completed and the placeholder has been removed.
StructuredViewerAdapter
Looking at the example it becomes clear how the DeferredContentManager
is able to support different viewer types. The viewer is adapted by the content manager using an suitable derivate of StructuredViewerAdapter
. For the time being there are only default adapters for abstract tree- and table viewers available.
However it is straight forward to write adapters for other structured viewer types. The following snippet shows e.g. the implementation for a ListViewer
:
public class ListViewerAdapter extends StructuredViewerAdapter { public ListViewerAdapter( AbstractListViewer listViewer ) { super( listViewer ); } @Override public void remove( Object element ) { viewer.remove( element ); } @Override public void addElements( Object parent, Object[] children ) { viewer.add( children ); } }
Using this and replacing the table viewer by a list viewer in the example would lead to the following outcome:
Cool! Isn’t it?
Conclusion
This post gave an introduction of DeferredContentManager
and showed how it enables background loading of model elements with different JFace Viewers. And if – after all the compelling usage explanations above – you might wonder where to get it, you will make a find at the Xiliary P2 repository. The content manager is part of the com.codeaffine.eclipse.ui
feature:
In case you want to have a look at the code or file an issue you might also have a look at the Xiliary GitHub project:
Reference: | Deferred Fetching of Model Elements with JFace Viewers from our JCG partner Frank Appel at the Code Affine blog. |