GuiProvider — Per-Entity GUI Services¶
Overview and Motivation¶
Almost everything the Rich Desktop Client does — searching, editing,
rendering tables and trees, drag & drop, context menus — is generic machinery
that works for any entity. What it cannot know is the entity-specific part:
which editor view to open for an Invoice, which icon a User shows in a tree,
which columns an Article table should display.
The GuiProvider<T> is the
single object that answers all of these questions for one PDO type. Exactly one
provider class exists per entity, and the framework consults it whenever a PDO
of that type meets the UI. It extends three smaller contracts:
PdoProvider<T>— the provider always operates on a concrete PDO instance (getPdo()).TableConfigurationProvider<T>— it supplies the table configuration describing how PDOs of this type appear in tables and tree-tables.DomainContextProvider— it carries the domain context of its PDO.
Applications practically never implement the interface from scratch. They extend
DefaultGuiProvider,
which implements the complete contract with sensible defaults, and override only
what differs — often just three methods. Together with the
default editor and default finder,
even an entity whose provider overrides nothing is already searchable,
viewable and editable through generated views.
Registration and Discovery¶
A provider is registered declaratively with the
@GuiProviderService
annotation, which names the serviced PDO interface:
@GuiProviderService(User.class)
public class UserGuiProvider extends DefaultGuiProvider<User> {
public UserGuiProvider(User pdo) {
super(pdo);
}
@Override
public Node createGraphic() {
return Fx.createGraphic(MyAppGraphicProvider.REALM, "user");
}
@Override
public boolean isEditorAvailable() {
return true;
}
@Override
public UserEditor createEditor() {
return Fx.load(UserEditor.class); // an FXML controller
}
}
@GuiProviderService is a @MappedService,
so the analyze build step records the PDO-class → provider-class mapping under
META-INF — no runtime classpath scanning, working identically in modular
(JPMS) and classpath deployments. At runtime the
GuiProviderFactory
(default: DefaultGuiProviderFactory)
resolves the mapping via a ClassMapper:
The factory looks up the provider class registered for the PDO's effective
class, picks the public constructor taking the PDO as its single argument
(hence the mandatory UserGuiProvider(User pdo) constructor above), caches the
constructor and instantiates a fresh provider per call. A provider is therefore
cheap, short-lived and always bound to one PDO instance —
never hold state in it beyond the PDO itself.
Because different modules can register a provider for the same PDO type, the
usual service override rules
apply: an application module can replace the provider that a lower-level module
(such as the framework's own
SecurityGuiProvider)
ships.
The annotation has two optional elements:
| Element | Default | Meaning |
|---|---|---|
noBundle |
false |
Declares that the provider ships no resource bundle (see Internationalization); avoids warnings in the i18n build goals. |
test |
true |
Whether the provider participates in the generic GuiProvider unit test. |
At compile time the
GuiProviderServiceAnnotationProcessor
validates the contract (annotated type implements GuiProvider, etc.) before
the application even runs.
The Contract at a Glance¶
The interface groups into six responsibilities:
| Concern | Methods | Consumed by |
|---|---|---|
| Appearance | createGraphic(), getBundle(), isBundleProvided() |
tree/table cells, editors, i18n |
| Editing | isEditorAvailable(), isEditAllowed(), isViewAllowed(), createEditor() |
PdoCrud, context-menu edit/view items |
| Searching | isFinderAvailable(), createFinder() |
PdoSearch |
| Tables | createTableConfiguration() (inherited), createTableView() |
search-result tables, Rdc table helpers |
| Trees | getTreeRoot(), getTreeText(parent), getToolTipText(parent), createTreeItem(), getTreeCellFactory(), providesTreeChildObjects(), getTreeChildObjects(parent), providesTreeParentObjects(), getTreeParentObjects(parent), getTreeExpandMaxDepth(), stopTreeExpansion() |
PdoTreeItem, PdoTreeCell, navigable object trees |
| Drag & drop | createDragboard(node), isDragAccepted(event), dropDragboard(dragboard) |
tables, trees, PDO-aware components |
The tree methods take the parent PDO as an argument because the same entity may render differently depending on where it hangs in a tree — an invoice under a customer may show a different text than the same invoice under a project.
DefaultGuiProvider — Out of the Box¶
DefaultGuiProvider
implements every method of the contract and is designed as an adapter: override
selectively, call super for the rest. What the defaults do:
| Method | Default behavior |
|---|---|
createGraphic() |
The generic "object" icon via Fx.createGraphic. |
isBundleProvided() / getBundle() |
Loads the provider bundle named after the provider class via BundleFactory; auto-detects its presence (see below). |
isEditorAvailable() |
false — an entity opts in to editing. |
isEditAllowed() / isViewAllowed() |
isEditorAvailable() && pdo.isEditAllowed() (resp. isViewAllowed()) — combines UI capability with the PDO's domain and security rules. |
createEditor() |
A generated DefaultPdoEditor (throws if isEditorAvailable() is false). |
isFinderAvailable() |
false — an entity opts in to searching. |
createFinder() |
The DefaultPdoFinder (throws if isFinderAvailable() is false). |
createTableConfiguration() |
Generated from the PDO model via reflection. |
createTableView() |
Creates, binds and configures an FxTableView from the table configuration. |
createDragboard(node) |
Every PDO is a drag source: puts the PDO's IdentifiableKey on the dragboard via RdcUtilities. |
isDragAccepted(event) |
false — a PDO does not accept drops unless it says so. |
dropDragboard(dragboard) |
Extracts the dropped PDOs from the dragboard and calls dropPdo(pdo) for each — override dropPdo to implement the drop. |
getTreeRoot() |
The PDO itself. |
getTreeText(parent) |
pdo.toString(). |
getToolTipText(parent) |
The PDO's long text if it implements ShortLongText, else the tree text. |
providesTreeChildObjects() / getTreeChildObjects(parent) |
false / empty — leaf node. |
providesTreeParentObjects() / getTreeParentObjects(parent) |
false / empty. |
getTreeExpandMaxDepth() / stopTreeExpansion() |
0 (no limit) / false. |
createTreeItem() / getTreeCellFactory() |
The standard lazily-expanding PdoTreeItem / PdoTreeCell via Rdc. |
Note the division of labor in the editing guards: isEditorAvailable() states
whether an editor exists for the entity, while isEditAllowed() and
isViewAllowed() additionally ask the PDO — and thus the domain logic and the
security layer — whether
the current user may edit or view this instance. The context menu grays out
its edit/view items accordingly.
Table configuration and table view¶
The provider is the TableConfigurationProvider
for its entity, so it decides how the entity appears in every
table and tree-table —
search results, related-PDO tables, tree-table columns.
createTableConfiguration() — the default builds the configuration from
the PDO model via reflection:
- Every model attribute whose getter is
@Bindablebecomes one column; the column name is the attribute's binding path. The bookkeeping attributes inherited fromPersistentObject(id,serial, …) and the object-id columns of relations are skipped. - The displayed column header comes from the provider's resource bundle
(key = attribute name) if one is provided, otherwise the capitalized attribute
name (
unitPrice→UnitPrice). - If no bindable attribute exists at all, the configuration falls back to
idandserialcolumns, so a table is never empty. - If a bundle is provided, its base name is stored in the configuration
(
setBaseBundleName) — used by the BundleMonkey translation tool to navigate from a rendered table back to the bundle that labels it.
createTableView() — turns the configuration into a live FxTableView:
it binds the columns according to the configuration's binding type (YES,
INHERITED or NO), asserts that all columns are actually bound (catching
typos in binding paths at once), creates the view via Fx.create(TableView.class)
and applies the configuration. The table name (default: the basename of the
PDO's effective class) keys the preferences
under which column widths, order, visibility and sorting are stored per user.
Subclasses that want hand-picked columns override createTableConfiguration()
and start from the empty-configuration helpers:
@Override
public TableConfiguration<Invoice> createTableConfiguration() {
TableConfiguration<Invoice> config = createEmptyTableConfiguration();
config.addColumn("invoiceNumber", getBundle().getString("invoiceNumber"));
config.addColumn("customer.name", getBundle().getString("customer")); // binding path across a relation
config.addColumn("total", getBundle().getString("total"));
return config;
}
createEmptyTableConfiguration(String name) creates a configuration with no
columns, applies the provider's bundle base name, and lets you pass a distinct
table name — useful when the same entity appears in differently laid-out
tables, since the name keys the table preferences.
The default editor¶
If a subclass turns on isEditorAvailable() without overriding
createEditor(), the PDO is edited in a
DefaultPdoEditor — a
form generated at runtime from the PDO model via reflection. This is more
than a stopgap: for simple master-data entities it is often all you need, and
during early development every entity is instantly editable before any FXML
exists.
The generated form is a two-column GridPane (right-aligned label, control),
one row per bindable model attribute, where the control type is chosen from
the attribute's type:
| Attribute type | Control |
|---|---|
Boolean / boolean |
CheckBox |
| enum | ChoiceBox |
| PDO relation, related entity has a preloading cache | ComboBox (populated from the cache) |
| PDO relation, no preloading cache | TextField |
any other type, model option LINES=n |
TextArea |
| any other type | TextField |
Field widths honor the model: an explicit COLS=n option wins, otherwise text
fields size themselves to the attribute's maximum column count. Every control
is fully wired into the binding layer,
so editing, validation
and save via the surrounding PdoCrud all work exactly as with a hand-written
editor. The initial focus goes to the first changeable domain-key attribute,
falling back to the first changeable control.
Labels come from the provider's bundle (key = attribute name — the same keys that label the table columns) or, without a bundle, from the attribute comments in the PDO model (see Internationalization).
For entities that may be inspected but not edited, wrap a plain view in a
PdoViewer and return false
from isEditAllowed() — see rdc.md.
The default finder¶
Likewise, turning on isFinderAvailable() without overriding createFinder()
yields the DefaultPdoFinder,
which covers the two most common search styles without any application code:
- If the entity provides a normtext (the normalized search text maintained
by the persistence layer), the finder shows a single pattern field
(input upper-cased automatically) and runs
selectByNormText(pattern). An empty pattern selects all. - If the entity has no normtext, there are no criteria to enter: the finder
hides itself and the search runs immediately —
selectAllCached()when the entity has a preloading PDO cache, elseselectAll().
The default finder is also useful verbatim in application providers, e.g. for small master-data entities where the result table is the search:
@Override
public PdoFinder<User> createFinder() {
@SuppressWarnings("unchecked")
PdoFinder<User> finder = Fx.load(DefaultPdoFinder.class);
finder.setSearchRunningImmediately(true); // populate the result table right away
return finder;
}
Entities with richer criteria implement their own
PdoFinder controller and
return it from createFinder(); the surrounding
PdoSearch machinery
(criteria area, result table, background execution, CRUD on double-click)
stays the same.
Internationalization¶
All texts a GuiProvider (and the views derived from it) presents are
localized through the framework's i18n foundation.
The texts resolve through four stacked levels — each level refines the one
below it, and each is optional:
level 4 database-backed translations (tentackle-i18n) runtime-editable overlay
level 3 locale-specific bundles UserGuiProvider_de_AT → ResourceBundle fall-back chain
UserGuiProvider_de
level 2 the provider bundle UserGuiProvider.properties
level 1 the PDO model attribute names and comments
Level 1: the PDO model¶
Without any bundle, all texts are derived from the
PDO model
itself: table column headers show the capitalized attribute name, and the
default editor labels its fields with the attribute comments from the model
(String name name the user's login name → label "the user's login name:").
The UI is fully functional but single-language — whatever language the model
is written in.
A provider that intentionally stays at this level declares
@GuiProviderService(value = User.class, noBundle = true) so the i18n build
goals do not warn about the missing bundle.
Level 2: the provider bundle¶
The next level is a .properties file co-located with the provider class
(in src/main/java, per the framework's resource conventions) and named after
it: UserGuiProvider.properties next to UserGuiProvider.java. The keys are
the attribute names — one set of keys labels both the table columns and
the default editor's fields, so entity terminology stays consistent across
views:
DefaultGuiProvider detects the bundle automatically: getBundle() loads it
through the module-aware
BundleFactory
(which is what makes co-located bundles work under JPMS, where a resource
bundle is normally invisible outside its own module), and isBundleProvided()
answers from the @GuiProviderService annotation if present — a first load
attempt otherwise. Because @GuiProviderService is itself a bundle-providing
annotation (analyzed by the BundleAnalyzeHandler), the bundle is recorded
under META-INF/bundles at build time and thus visible to the i18n tooling and
the runtime alike.
Custom editors and finders are @FxControllerService controllers with their
own co-located bundles, but the provider bundle remains available to them:
createEditor() for the default editor passes it along, and application
editors typically reuse the provider's keys for shared terminology.
Level 3: locale-specific bundles¶
Translations are added per locale using the standard ResourceBundle
fall-back chain — UserGuiProvider_de_AT → UserGuiProvider_de →
UserGuiProvider.properties — so a more specific locale only overrides the
keys that actually differ:
Which locale applies is decided per thread by the
LocaleProvider.
On the client that is simply the user's locale; in a multi-tier deployment the
server sets the thread-local locale per remote request from the calling
client's SessionInfo — one server serves every client in its own language,
including texts produced inside GuiProvider-related code paths that run
remotely.
At build time the check plugin verifies that referenced keys resolve, and the i18n maven plugin syncs the property files with the translation database.
Level 4: database-backed translations¶
With the optional tentackle-i18n
module, stored translations overlay the property files at runtime — no
change to the provider or its bundles. A translator edits a text live (e.g.
with the BundleMonkey tool); the modification tracker invalidates the bundle
caches, and the new label appears on every client immediately. This is also
where the baseBundleName recorded in the table configuration pays off: the
tooling can navigate from a displayed table or editor straight to the bundle
and key that produced a label.
The framework's own texts (context menus, CRUD toolbar, dialogs — from bundles
such as RdcFxRdcBundle) ship already localized at levels 2–3, so an
application only translates what it adds: the entity terminology in its
provider bundles and its own views.
Testing¶
tentackle-test-fx-rdc
ships a generic GuiProviderTest base class (TestNG and JUnit 5 flavors) that
iterates over all registered @GuiProviderServices, instantiates each
provider against a fresh PDO and verifies that it can be created and bound —
catching missing constructors, broken bindings or unresolvable editors in the
unit-test phase. A single subclass suffices:
public class TestGuiProviders extends GuiProviderTest {
TestGuiProviders() {
super("com.example.myapp"); // restrict to the application's providers
}
}
Providers annotated with @GuiProviderService(..., test = false) are skipped.
See Also¶
- tentackle-fx-rdc — Rich Desktop Client — the module overview: CRUD machinery, trees, context menus, lifecycle.
- Tables and Trees — the table configuration infrastructure the provider feeds.
- Resource Bundles — bundle convention,
BundleFactory,LocaleProvider. - Tentackle I18N — Database-backed Resource Bundles — the live-editable translation overlay.
- Services — the
@MappedServicediscovery behind@GuiProviderService. - Model Definition — attributes, comments and options the generated views build on.
- tentackle-test-fx-rdc — testing GuiProviders.