Skip to content

Tables and Trees — Configuration-Driven Tables, Tree-Tables, and Views

Overview and Motivation

Tables are the workhorse of business applications: search results, pick lists, journal views, editable detail grids. Plain JavaFX makes each of these a chore — every TableColumn needs a cell-value factory, a cell factory, a formatter, an alignment, an editor, and none of it survives a restart: column widths, ordering, visibility and sorting are gone when the user closes the window.

Tentackle replaces that per-column boilerplate with a declarative table configuration: a single TableConfiguration object describes the table by binding paths, and everything else — cell rendering, editors, formats, preferences-backed layouts, totals, printing, spreadsheet export — derives from it:

  • One configuration, two view types — the same TableConfiguration configures both an FxTableView and an FxTreeTableView.
  • Columns are binding pathsconfig.addColumn("orgUnit", "Org Unit") is all it takes; the column's type, format, alignment and editor follow from the bound model member.
  • Rendering by value class — a pluggable TableCellType knows how to display and edit one Java type, application-wide.
  • Layouts that roam — column widths, order, visibility and sorting are stored in the persisted preferences, per user or system-wide; with database-backed preferences the layout follows the user to any workstation.
  • PDO awareness on top — the rich desktop client derives default configurations from the PDO model, adds a column-chooser popup, printing, and spreadsheet/XML export (tentackle-fx-rdc-poi upgrades the export to real Excel workbooks).

The foundation lives in org.tentackle.fx.table (+ table.type) in this module; the PDO layer in org.tentackle.fx.rdc.table of tentackle-fx-rdc is covered below.

Key Concepts

The view components

TableConfiguration — the single source of truth

A TableConfiguration<S> describes a table of row type S. Its name doubles as the key to the table's preferences. Columns are added by binding path:

TableConfiguration<Invoice> config = FxFactory.getInstance().createTableConfiguration(Invoice.class, null);
config.addColumn("invoiceNumber", bundle.getString("invoiceNumber"));
config.addColumn("total", bundle.getString("total"));
config.setEditMode(TableConfiguration.EDITMODE.ROW);
config.configure(tableView);      // creates columns, cells, editors, applies preferences

configure(...) accepts either an FxTableView or an FxTreeTableView — the configuration is view-type agnostic (for tree-tables it also creates the TreeItems via createTreeItem).

Two enums control the overall behavior:

  • BINDING — whether the cells are bound to the row object's members: YES (the default, binds all @Bindable members), INHERITED (includes bindables inherited from superclasses) or NO.
  • EDITMODENO (read-only, the default), SIMPLE (edit the current cell only), ROW (commit moves to the next/previous cell in the row) or COLUMN (commit moves to the cell below/above).

The configuration owns an FxTableBinder that binds the cells to the row objects. Don't confuse it with the controller's binder, which binds a data list to the table as a whole: binding properties needed by cell editors — the DomainContext, for instance — must be applied to the table binder (see binding.md).

TableColumnConfiguration — per-column tuning

Each column is described by a TableColumnConfiguration<S,T>, whose name is the binding path (and the column's preferences key). Everything a business grid needs is a property, with null meaning "use the default derived from the column type or editor":

Aspect Properties
Display displayedName, alignment, pattern (yields a DecimalFormat or DateTimeFormatter), blankZero
Editing editable, editor (an FxComponent overriding the cell type's default), validChars / invalidChars, unsigned, scale, maxColumns, caseConversion, autoSelect
Behavior summable (defaults to true for numeric types — see totals), cellType (overrides the type-derived cell type), binding

The configuration creates and caches the actual TableColumn or TreeTableColumn on demand (getTableColumn() / getTreeTableColumn()).

Table cell types — rendering per value class

A TableCellType<T> is a singleton that knows how to display (updateItem) and edit (getEditor) values of one Java class in table and tree-table cells. Cell types are produced on demand by the TableCellTypeFactory and registered with @TableCellTypeService, a mapped service keyed by the value class — so an application can add cell types for its own datatypes or replace the provided ones.

The org.tentackle.fx.table.type package ships cell types for strings, numbers, booleans, enums, BMoney, I18NText, plain objects, and the full date/time family (Date, Time, Timestamp, LocalDate, LocalTime, LocalDateTime, Instant, OffsetTime, OffsetDateTime, ZonedDateTime), plus abstract bases (AbstractTableCellType, AbstractDateTimeTableCellType) for custom types.

Table configuration providers

A TableConfigurationProvider creates the TableConfiguration for one row class. Providers are registered via @TableConfigurationProviderService (again a @MappedService) and located through the TableConfigurationProviderFactory, which maps the row class leniently — a provider registered for a supertype or interface serves the subclasses as well. This is how generic UI such as the RDC search dialog obtains the right columns for any row type without knowing it in advance. In RDC applications the per-PDO GuiProvider is the table configuration provider (see below).

Preferences-backed layouts

TableConfiguration.savePreferences(...) / loadPreferences(...) persist and restore the visual state of a table via the pluggable persisted preferences:

  • What is stored: per column the visibility, width, view position and sort type/order, plus the table's preferred width and height. Restoring the sorting is optional (setSortingIncluded, default off); restoring the view size can be turned off (setViewSizeIncluded, default on).
  • Where it is stored: under a node named after the configuration's name, an optional suffix (to distinguish several tables of the same type, e.g. per controller), and the effective locale — column headers translate, so layouts are locale-specific. Each column gets a child node keyed by its binding path.
  • User vs. system scope: layouts save to the user's preferences by default; loading falls back to the system preferences when the user hasn't stored a layout yet. Saving to the system scope turns a layout into the default for all users.

In a Tentackle application the preferences are usually database-backed, so a user's table layouts follow them to any workstation — and system-wide defaults are rolled out by simply saving them once.

Totals

TotalsTableView is a small companion table displayed below (or wherever the layout puts it) a regular FxTableView. Bound via setBoundTable, it mirrors the bound table's columns — widths and visibility are bound, the header and horizontal scrollbar are hidden via its own CSS — and renders one row per aggregate the application supplies as items (one row for totals; three for totals, minimum and maximum; …). Which columns show a total is controlled by the column configuration's summable property, defaulting to true for numeric column types.

The RDC Layer: PDO-Aware Tables

The rich desktop client builds on this foundation in org.tentackle.fx.rdc.table and makes tables of PDOs essentially free.

GuiProviders supply the configuration

Every PDO's GuiProvider extends TableConfigurationProvider. The DefaultGuiProvider derives a default configuration from the PDO model via reflection: each @Persistent attribute with a @Bindable getter becomes a column, displayed names come from the provider's resource bundle. createTableView() then creates the table, applies the binding mode and asserts that all columns are bound. A typical override picks and orders the columns explicitly, using the generated attribute-name constants:

@Override
public TableConfiguration<Message> createTableConfiguration() {
  TableConfiguration<Message> config = createEmptyTableConfiguration();
  config.addColumn(Message.AN_MESSAGENUMBER, getBundle().getString(Message.AN_MESSAGENUMBER));
  config.addColumn(Message.AN_WHEN, getBundle().getString(Message.AN_WHEN));
  config.addColumn(Message.RN_ORGUNIT, getBundle().getString(Message.RN_ORGUNIT));
  return config;
}

This is what the RDC search dialog uses for every result table: guiProvider.createTableView() plus a table popup — no per-entity table code at all.

RdcTableConfiguration and RdcTableColumnConfiguration are the PDO-aware configuration implementations, and PdoTableCellType registers a cell type for the PersistentDomainObject interface itself, so PDO-valued columns (relations, for example) render out of the box.

TablePopup — the user's table menu

TablePopup (created via Rdc.createTablePopup(table, preferencesSuffix, title)) attaches a context menu to any table or tree-table:

  • a column chooser with per-column visibility checkboxes and an auto adjust width action;
  • print with a preview, via the TablePrinter controller;
  • export to spreadsheet and export to XML, each for all or only the selected rows;
  • save / load preferences — the user's own layout, plus loading the system-wide default; saving the system layout is offered when the application runs with system-only preferences (an administrative mode), and the save items disappear entirely when the preferences are read-only;
  • expand / collapse all for tree-tables.

TableUtilities — replaceable table services

TableUtilities is the @Service singleton behind the popup's actions: creating default configurations from PDO classes, printing, and exporting. The stock implementation writes RFC-4180 CSV for the spreadsheet export — dependency-free. Adding the tentackle-fx-rdc-poi module to the path replaces the service with one that writes styled, typed Excel workbooks via Apache POI, without touching application code.

How It Fits Together

A controller displaying a list of invoices in an editable table:

  1. The FXML declares an FxTableView (or the code creates one via guiProvider.createTableView() in an RDC application).
  2. The table configuration — from a TableConfigurationProvider, the PDO's GuiProvider, or built inline — is applied with config.configure(tableView): columns, cell types, editors and formats appear, and the saved layout is restored from the preferences.
  3. The controller's binder binds the data list to the table by name; the table's own binder moves values between cells and row objects, translating and validating through the same machinery as any form field.
  4. Rdc.createTablePopup(...) adds the column chooser, printing, export, and layout save/load.
  5. When the user rearranges columns and saves, the layout lands in the (database-backed) preferences and greets them on every workstation from then on.

Package Layout

Package Content Module
org.tentackle.fx.table Configurations, providers, cell/column classes, TotalsTableView tentackle-fx
org.tentackle.fx.table.type Built-in TableCellTypes (strings, numbers, money, date/time, …) tentackle-fx
org.tentackle.fx.rdc.table PDO-aware configuration, TablePopup, TablePrinter, TableUtilities tentackle-fx-rdc
org.tentackle.fx.rdc.poi Excel export replacing the CSV default tentackle-fx-rdc-poi