DomUI

Table models

A table model is where the rows come from. Every table-shaped component in this group takes an ITableModel<T>, and none of them care which one it is.

//-- A query
SimpleSearchModel<Album> queried = new SimpleSearchModel<>(this,
    QCriteria.create(Album.class).ascending(Album_.title()));

//-- A list you hold yourself
SortableListModel<BasketLine> held = new SortableListModel<>(BasketLine.class, lines);

What a model must answer

ITableModel<T> is small: how many rows are there (getRows()), give me rows a to b (getItems(a, b)), and tell these listeners when something changes. Everything else is added by the interfaces a model may also implement:

Interface Adds
IModifyableTableModel<T> add(), delete(), modified() - changing the data through the model
ISortableTableModel sortOn(property, descending) - what a header click calls
ITruncateableDataModel isTruncated() - "there were more rows than I fetched"
IKeyedTableModel<T> rows can be found by primary key
IShelvedListener the model is told when its page is shelved and returned to

The models that ship

Model Rows come from Sorts
SimpleSearchModel<T> a QCriteria, an IQuery or a query functor in the database, by re-running the query
SimpleListModel<T> a List<T> you give it not at all
SortableListModel<T> the same in memory, with a comparator per property
DefaultTableModel<T> a List<T>, kept modifiable in memory
SimpleKeyModel<T,K> a list, addressed by key -

Where the sorting happens matters as soon as the result is bigger than one page: the query model sorts before the row limit, so sorting 20,000 tracks gives the first page of all of them. A list model can only sort the list it holds.

SimpleSearchModel

The model most list screens use. It runs its query once, keeps the result, and hands out the slice each page needs - paging costs nothing.

Method What it does
new SimpleSearchModel<>(NodeBase, QCriteria<T>) the usual form: the query, run in the page's own context
new SimpleSearchModel<>(NodeBase, IQuery<T>) a lambda that runs the query itself, given the data context, the sort property and the row limit
setRefreshAfterShelve(boolean) re-query when the page is returned to
getQuery() the criteria it holds
setMaxRowCount(int) how many rows to fetch at most; ITableModel.DEFAULT_MAX_SIZE (1000) when unset
isTruncated() whether the result hit that maximum

It fetches at most 1000 rows by default and reports isTruncated() when there were more, which the table shows as a truncation marker. It is also an IShelvedListener: when its page is shelved it drops the result, so returning to the screen re-runs the query rather than showing what was true an hour ago.

Changing rows

For a list model, every change goes through the model:

listModel.add(new BasketLine("Let It Be", 1));   // appears in the table
line.setCopies(line.getCopies() + 1);
listModel.modified(0);                          // that one cell updates
listModel.delete(0);                            // that row disappears

Each of those tells the table exactly what changed, and the table updates exactly that - no rebuild, no flicker, and the scroll position and selection survive. Changing the list itself and not telling the model leaves the screen showing the old rows.

An IObservableList<T> can be handed to the table directly with setList(); the table then listens to the list and no model is written at all. That is the usual way to bind a table to a relation of a record - see data binding in a table.

Last modified: 2026/09/02