Thursday, November 22, 2012

Lazy loading CDO backed JFace Viewer.

This post is about achieving a good user experience in a User Interface while presenting a potentially large set of data which is retrieved from a back-end system. In our case the UI is an Eclipse RCP Application, which uses JFace Viewers and the Back-end system is Connected Data Objects, in short, CDO. CDO is an object-persistence middleware system based on the Eclipse Modeling Framework (EMF).

Objective

The objective is to load data in a UI Widget "Just in time". In this case it means, whenever data becomes visible. The Eclipse JFace library as such a facility for scrolling through a TableViewer or TreeViewer. For this to work the following conditions need to be met:
  1. The Content Provider needs to be an ILazyContentProvider for TableViewers and a ILazyTreeContentProvider for TreeViewers. 
  2. As data is fetched dynamically the content provider is not knowledgeable about the number of items in the viewer. Therefor it is required to call setItemCount(...) on the viewer. 
  3. The TableViewer should be created with the SWT.VIRTUAL style to signal that the viewer should be updated only when data becomes visible.
An example of such a content provider implementation could look like this
public class LazyListContentProvider implements ILazyContentProvider {
 
 private Viewer viewer;
 private List content;

 public void dispose() {

 }

 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
  this.viewer = viewer;
  this.content = (List) newInput;
 }

 public void updateElement(int index) {

  if (viewer instanceof TableViewer) {
   ((AbstractTableViewer) viewer).replace(content.get(index), index);
  } else {
   throw new UnsupportedOperationException(
     "Only table viewer is supported");
  }
}


Here updateElement(int index) will be called, whenever more items are needed which stems from the use scrolling the viewer down and revealing more rows.

The code for the table viewer would something like this

tblViewer = new TableViewer(frmTolerances.getBody(),
SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION |SWT.VIRTUAL| widgetStyle);

tblViewer.setItemCount(((CDOResource)resource)     .eContents().size());

tblViewer.setContentProvider(new LazyListContentProvider());
tblViewer.setLabelProvider(....any label provider...);
tblViewer.setInput(toleranceResource.getContents());

Filters, Sorting and History


Now there is one limitation with using an ILazyContentProvider. It's not capable to deal with the sorting and filtering capabilities of a JFace Viewer. Why not? Well, the viewer doesn't have access to to the complete set of items, and as both Sorting and Filtering require the full set of items, it won't work.

However there is a potential solution to enable these functions, and if you are a user of Eclipse, the chance is you use this already on a daily basis. The same dilema exited for finding types in the IDE and other search operations which require large sets of data to be presented, filtered, remembered and sorted. The implementation of this function is the FilteredItemSelectionDialog. Now if we look at this class we notice that it complies to all requirements for Lazy loading data. It uses a content provider which implements the ILazyContentProvider. It acts on a TableViewer with flag SWT.Virtual and sets the item count. The really nifty thing however is all the additional facilities available as well.

As an example and teaser here is a screenshot of a view (In this case in an Eclipse Form) uses the base class referred further down.




Filtering 

The concept of filtering in the FilteredItemSelectionDialog should be decomposed, to reflect what happens when filtering.

The dialog has a text / search entry widget which allows a filtering pattern to be applied on the data set. The way it works is by adding items to a dedicated filtered items collection which match the filter criteria. For this the Content Provider needs to be fed with items when realized. As the Dialog is abstract, a concrete implementation will implement the methods:
@Override
  protected void fillContentProvider(
    AbstractContentProvider contentProvider,
    ItemsFilter itemsFilter, IProgressMonitor progressMonitor)
    throws CoreException {
  ...
  }

 @Override
  protected ItemsFilter createFilter() {
   return null;
...
  }

The concrete implementation method will feed the content provider with the data, considering the provided filter. Note that the filter is applied already when realizing the Dialog. the method which initiates activities is applyFilter() . It is also called when the pattern matching input text changes.

applyFilter() will invoke a sequence of background activities, which use the Eclipse Job API. The sequence is:

(1) applyFilter()
           |___ (1a) createFilter()
           |
(2) FilterHistoryJob -- (4) RefreshCacheJob -- (5) RefreshJob
           |
(3) FilterJob -- (4) RefreshCacheJob -- (5) RefreshJob
         
(6) RefreshProgressMessageJob

1a ItemsFilter

The ItemsFilter is abstract and should be implemented by Clients. A typical implementation will extract a relevant item attribute like the "name" of the item and match it against the filter.
This is as easy as calling the .matches(String) method on the ItemsFilter class.

Note that the ItemsFilter can be instantiated with a custom SearchPattern(int rule) class. The SearchPattern can hold various matching rules like exact matching or pattern matching and case sensitive matching.

2 FilterHistoryJob

First, calls the createFilter() method, which will call the concrete implementation. Next it will invoke a background Job which populates the items with a potential history. (Still considering the filter). This job is named: FilterHistoryJob. The history is managed by an abstract SelectionHistory class, which clients should extend. The purpose the ability to customize the serialization to XML for the items which need to be remembered. Items can be added to the History as we wish. In the case of the FilteredItemSelectionDialog, selected items are remembered when OK is pressed. This will actually only happen when the SelectionHistory has been set using:


.setSelectionHistory(SelectionHistory);


In our implementation, As we deal with CDO Objects, we use the unique CDOID as a serialization option.

It will already refresh the viewer with the result from History if any and if the filter matches the history item,  but will also as a last step invoke the next Job which is the FilterJob.

3 FilterJob

The FilterJob will do the actual filtering based on the current ItemsFilter as created in the first step. This is delegated to the method filterContent() in the job.

4 RefreshCacheJob

The RefreshCacheJob will refresh the viewer with the result of the filtered items in two steps. First the cache is is refreshed by the procedure described here:

The filtered items is a result collection of several actions:

  1. First the current items are sorted first using a Client specified Comparator. 
  2. Next a potential additional ViewerFilter is applied. The ViewerFilter(s) can be defined by calling addFilter(ViewerFilter).  
  3. Finally a Separator is inserted in the cache between the Historical items and any potential item not in history which as previously made it through the Item filter and the Additional ViewerFilter(s) 

The second and last step is to invoke another job which is the RefreshJob

5 RefreshJob

This is a UIThread Job as it is actually refreshing the TableViewer. As we use a ILazyContentProvider, what happens is to set the item count on the table viewer with method setItemCount(). Also the selection is saved and restored after the TableViewer refresh. Note that a refresh will trigger the ILazyContentProvider update method, which will in turn replace the viewer items with the cached items. 

6 RefreshProgressMessageJob

This job refreshes the loading progress, it's cyclical so it schedules itself every 500 milliseconds while the progress is not cancelled or done. 

Applying the method on a lazy loading 

So now we understand the FilteredItemSelectionDialog inner workings, we can apply this same technique to a UI Component which is not a Dialog, but perhaps a ViewerPart which presents CDO Data.  The steps to do so are:

  1. Extract the relevant code from FilteredItemSelectionDialog
  2. Change the algorithm for populating the initial items list. 

Refactor FilteredItemSelectionDialog 

There is very various things we want to do:

  • Remove Dialog specifics 

Here we want to remove all the Dialog specific stuff, and refactor this is as a "regular" Eclipse UI Component. Various aspects like Saving Dialog Settings, and handling button selection are not required so can be removed. 
  • Support for multiple columns in the TableViewer
Also we want to support multiple columns. For this we need to provide a hook for clients to create the columns, additionally we want the default implementation of the Label Provider. (Which is very sophisticated), to also support multiple columns. This is done by supporting the ITableLabelProvider


With a bit of work, you could get an abstract like this: AbstractLazyTableViewer

This is merely an example, but it is self-contained and should work for clients extending it. 
Note 1: The refactored version assumes it will be based in an Form. 

Algorithm for populating the inital items list

The implementation will, without adaptations, populate the entire content in the viewer, if a pattern like "?" is entered. Although an ILazyContentProvider is used, it's not acting entirely as we might expect. What happens is that the method fillContentProvider expect the following method to be called:

public void add(Object item, ItemsFilter itemsFilter)

Now that's Ok for a data source which is local, but in case of a remote CDO Repository, it forces hard labour instead of laziness. So we need to come up with a different solution. Unfortunately I am running out of time now.. so perhaps thought for another post. 

No comments:

Post a Comment