Building tables made easy: TableViewerBuilder

In my last RCP training the participants complained a bit about the JFace TableViewer API. I have to agree, tables are the bread and butter of many RCP business applications and binding tables to your data model is a somewhat cumbersome process that usually yields a lot of boilerplate code.

This was eased a bit with the latest additions to JFace Data Binding, which allow you to use data binding to bind the columns of TableViewers. Have a look at the JFace data binding snippets Snippet017TableViewerWithDerivedColumns and Snippet032TableViewerColumnEditing to find out how that works. JFace Data Binding is perfect if the binding between the table UI and the tabel model data is bi-directional.

But many table bindings are a passive thing, they display and modify data in a model object but the model object doesn’t change by itself and you wouldn’t expect the table to refresh automatically if the model changes. This use case is usually implemented using TableViewer and custom CellLabelProviders.

I just finished a builder class TableViewerBuilder, which makes it easy to setup TableViewers. Features of TableViewerBuilder are:

Example

Let’s say we want to build such a table:

Table

If you want to see a full example right away, here you go: Snippet01TableViewerBuilder

TableViewerBuilder

At first you create a TableViewerBuilder. This instantly creates a Table widget and a TableViewer for you. The given parent Composite needs to be empty, because TableColumnLayout is used internally. For example:

TableViewerBuilder t = new TableViewerBuilder(parent);

Columns

You can create columns by calling createColumn on the TableViewerBuilder object. This returns a ColumnBuilder that can be used to configure the table column. When you have finished configuring the column, you have to call build() on the ColumnBuilder to create the actual column:

TableViewerBuilder t = new TableViewerBuilder(parent);
t.createColumn("City").build();
t.createColumn("Population").build();
t.createColumn("Area").build();
t.createColumn("People/km²").build();
t.createColumn("Founding date").build();
t.createColumn("Neighbor city").build();

This is the result:

Setting the input data

To set the data to be shown in the table, you can get the TableViewer from the TableViewerBuilder and set a ContentProvider and Input object:

t.getTableViewer().setContentProvider(someContentProvider);
t.getTableViewer().setInput(someInput);

If you want to use a Collection as model for your table, you can also use setInput on the TableViewerBuilder with a Collection - this automatically sets an ArrayContentProvider for you:

t.setInput(someCityCollection);

Binding columns

In this example someCityCollection is a list of City objects. The columns of the table can be bound to a property of these objects using bindToProperty:

ColumnBuilder city = t.createColumn("City");
city.bindToProperty("name");
city.build();

ColumnBuilder population = t.createColumn("Population");
population.bindToProperty("stats.population");
population.build();

ColumnBuilder area = // ...
ColumnBuilder density = // ...
ColumnBuilder foundingDate = // ...
ColumnBuilder neighborCity = // ...

You can also bind a column to an arbitrary value using bindToValue. In the example this helps with the “People/km²” column which is calculated from two other values:

ColumnBuilder density = t.createColumn("People/km²");
density.bindToValue(new BaseValue<City>() {
    @Override
    public Object get(City city) {
        return city.getStats().getPopulation() / city.getStats().getAreaKm2();
    }
});
density.build();

This is the result. Please note that the user can already sort the table by clicking on the table headers:

Formatting values

In this example, the number and date values need to be formatted. You can set a formatter on the column objects for this:

population.format(Formatter.forInt(new DecimalFormat("#,##0")));
area.format(Formatter.forDouble(new DecimalFormat("0.00 km²")));
density.format(Formatter.forDouble(new DecimalFormat("0")));
foundingDate.format(Formatter.forDate(SimpleDateFormat.getDateInstance(SimpleDateFormat.MEDIUM)));

Formatter is a factory class for commonly used formatters (mainly based on java.text.Format). You can always implement the IValueFormatter interface yourself. Please note that formatting has no influence on the sort order, because the comparator responsible for the table sorting uses the raw data values, not the formatted values.

Formatting cells

Sometimes you want to format the cell besides the textual value, for example to customize colors or to set images. You can do that by configuring a cell formatter for the table column:

population.format(new ICellFormatter() {

    public void formatCell(ViewerCell cell, Object value) {
        int population = (Integer) value;
        int color = (population > 5000000) ? SWT.COLOR_RED : SWT.COLOR_BLACK;
        cell.setForeground(cell.getControl().getDisplay().getSystemColor(color));
    }

});

If your column is not text based (for example a column with images that are owner-drawn), you can use a custom CellLabelProvider instead of a value and a value formatter:

someColumn.setCustomLabelProvider(new CellLabelProvider() { /* ... */ });

The result so far:

Column width and align

Use setPercentWidth, setPixelWidth and align to format the cell width and alignment:

city.setPercentWidth(60);
foundingDate.setPixelWidth(100);
foundingDate.alignCenter();
area.alignRight();

Sorting

You can set the default sort column by calling useAsDefaultSortColumn:

city.useAsDefaultSortColumn();

If you want to sort the column by a value that is different from the original value, you can set a custom value using sortBy:

city.sortBy(new PropertyValue("stats.otherValue"));

Cell editing

You can make columns editable using makeEditable. By default, you get a text cell editor with no formatting applied:

city.makeEditable()

For editing formatted values, you need to specify a formatter which is also responsible to parse the value back from the String:

population.makeEditable(Formatter.forInt());
area.makeEditable(Formatter.forDouble(new DecimalFormat("0.00")));
foundingDate.makeEditable(Formatter.forDate(SimpleDateFormat.getDateInstance(SimpleDateFormat.MEDIUM)));

You can also set your own cell editor, for example:

ComboBoxViewerCellEditor cityComboEditor = new ComboBoxViewerCellEditor(t.getTable(), SWT.READ_ONLY);
cityComboEditor.setContenProvider(new ArrayContentProvider());
cityComboEditor.setLabelProvider(new LabelProvider());
cityComboEditor.setInput(RandomData.CITIES);

neighborCity.makeEditable(cityComboEditor);

Result:

Download

I hope this helps with building JFace TableViewers.

You can see the full example code here: Snippet01TableViewerBuilder.

TableViewerBuilder is part of my de.ralfebert.rcputils plug-in which can be downloaded as source project from github: de.ralfebert.rcputils

Apps

website screenshot mac os x

About the author

Ralf Ebert Ralf Ebert is an independent software developer and trainer for Mac OS X and iOS. He makes the Page Layers and Straight ahead apps and conducts iOS trainings in Germany since 2009.