tentackle-fx-rdc — Rich Desktop Client¶
Overview and Motivation¶
RDC stands for Rich Desktop Client. The tentackle-fx-rdc module is the
glue that turns the two lower layers it sits on — the
PDO layer and the
extended JavaFX layer — into a
ready-to-run desktop application.
Where tentackle-fx knows everything about JavaFX controls, controllers and
binding but nothing about persistence, and tentackle-pdo knows everything about
persistent domain objects but nothing about a UI, tentackle-fx-rdc is the only
module that knows about both. It answers the questions that arise as soon as a
PDO needs to be shown to and edited by a user:
- How is a PDO searched for, displayed, edited and saved through a UI?
- How does a PDO render itself in a table, a tree or a combo box?
- What happens when the user right-clicks a PDO — which context menu appears?
- How does the whole application start up, log in against a backend and shut down again?
The module is deliberately built around a small number of extension points so that an application provides only the parts that are specific to its entities, while the framework supplies the generic CRUD machinery, dialogs, background execution and lifecycle.
Design principles¶
- One coordination object per entity. Everything the UI needs to know about
a PDO type is bundled behind a single interface — the
GuiProvider. Applications implement (or partially override) it per entity; the framework discovers the implementation via the SPI mechanism. - Generic CRUD, specific views. The search-, edit- and tree machinery
(
PdoSearch,PdoCrud,PdoController, …) is entity-agnostic. The only entity-specific artifacts an application must supply are the editor view, the finder view and the table/tree configuration. - Never block the FX thread. Persistence operations may go to a local database or across the network. RDC funnels them through a replaceable background executor and marshals the results back onto the FX application thread (see Background execution).
- Location transparency, inherited. Because PDOs are remoting-capable, the
same UI code works whether the client talks to the database directly or through
a middle-tier server — no
LazyInitializationException, ever. - Convention with escape hatches. Defaults (
DefaultGuiProvider,DefaultPdoFinder,DefaultPdoEditor,DefaultRdcFactory) cover the common case; every one of them can be replaced or subclassed.
Where it sits¶
tentackle-fx-rdc ── knows PDOs *and* JavaFX ─────────────┐
│ requires transitive │
├── org.tentackle.fx (controls, controllers, │ the only module
│ binding, tables) │ that bridges the
└── org.tentackle.pdo (persistent domain │ UI and the
objects, sessions, DDD) │ persistence world
┘
The companion modules build on top of it:
| Module | Purpose |
|---|---|
tentackle-fx-rdc-poi |
Excel (Apache POI) export of RDC tables. |
tentackle-fx-rdc-update |
Application self-update / deployment support. |
tentackle-test-fx-rdc |
JUnit 5 / TestNG base classes for testing GuiProviders, controllers and table configurations. |
Key Concepts¶
GuiProvider — the per-entity hub¶
The GuiProvider<T> is the
central abstraction of the module — it has its own in-depth document,
GuiProvider — Per-Entity GUI Services, covering the
DefaultGuiProvider defaults, the generated table configurations, the default
editor and finder, and the internationalization levels. In short: one
GuiProvider exists per PDO type and encapsulates all the UI behavior for
that entity. Its responsibilities fall into a few groups:
- Identity / appearance —
createGraphic()for an icon, the resource bundle (getBundle(),isBundleProvided()). - Editing —
isEditorAvailable(),isEditAllowed(),isViewAllowed(),createEditor()returning aPdoEditor<T>. - Searching —
isFinderAvailable(),createFinder()returning aPdoFinder<T>. - Drag & drop —
createDragboard(...),isDragAccepted(...),dropDragboard(...). - Tree rendering —
getTreeText(...),getToolTipText(...),createTreeItem(),getTreeCellFactory(), and the child objectsprovidesTreeChildObjects()/getTreeChildObjects(...). The remaining tree methods (getTreeRoot(),getTreeParentObjects(...),getTreeExpandMaxDepth(),stopTreeExpansion()) are hooks for application-specific navigable trees; the framework's own trees don't call them. - Table rendering —
createTableView().
Applications rarely implement the whole interface. They subclass
DefaultGuiProvider and
override only what differs. A provider is registered declaratively:
@GuiProviderService(Invoice.class)
public class InvoiceGuiProvider extends DefaultGuiProvider<Invoice> {
@Override
public PdoEditor<Invoice> createEditor() {
return Fx.load(InvoiceEditor.class); // an FXML controller
}
@Override
public PdoFinder<Invoice> createFinder() {
return Fx.load(InvoiceFinder.class);
}
}
The @GuiProviderService
annotation is picked up at build time by the
analyze phase, which generates the
META-INF/services entry, so the provider is found by
GuiProviderFactory at
runtime — in both modular and non-modular deployments. Independently, an
annotation processor validates at compile time that the
provider satisfies the contract.
Read-only views with PdoViewer¶
When an entity can be looked at but not edited, wrap a plain view controller in a
PdoViewer and disable editing:
@Override
public PdoEditor<Incident> createEditor() {
return new PdoViewer<Incident, IncidentView>(Fx.load(IncidentView.class));
}
@Override
public boolean isEditAllowed() {
return false; // grays out the "edit" context-menu item
}
Rdc and RdcFactory — the entry points¶
Rdc is a static convenience facade
(mirroring the role Fx plays in tentackle-fx) that bundles the most common
operations so calling code reads naturally. The heavy lifting is delegated to the
SPI-resolved RdcFactory
(default: DefaultRdcFactory).
Typical one-liners:
// open a non-modal edit dialog for a PDO
Rdc.displayCrudStage(invoice, /*editable*/ true, ownerWindow);
// open a non-modal search dialog for an entity, allowing to create new ones
Rdc.displaySearchStage(on(Invoice.class), ownerWindow, /*createPdoAllowed*/ true);
// render a PDO in a tree
Rdc.showTree(owner, rootPdo);
// build cells / tree items for a table or tree
TreeItem<Invoice> item = Rdc.createTreeItem(invoice);
Rdc also exposes the standard save / discard / cancel dialog
(showSaveDiscardCancelDialog), stage-hierarchy helpers
(closeStageHierarchy) and the background-execution helpers described below.
CRUD controllers: PdoController, PdoCrud, PdoSearch¶
The generic editing machinery is layered:
| Type | Role |
|---|---|
PdoController<T> |
Base FX controller bound to a single PDO (setPdo). |
PdoEditor<T> |
A controller that edits a PDO; supplied by the GuiProvider. |
PdoFinder<T> |
Holds the search-criteria view and runs the query. |
PdoCrud<T> |
The full create/read/update/delete controller wrapping an editor with toolbar, navigation and persistence actions. |
PdoSearch<T> |
The search controller wrapping a finder with a result table that opens CRUD on selection. |
The default finder,
DefaultPdoFinder,
provides a normtext search field when the entity defines a normtext, and
otherwise selects all PDOs immediately — so even an entity with no custom finder
view is searchable out of the box.
Configuring CRUD and search dialogs¶
Rdc.displayCrudStage(...) and Rdc.displaySearchStage(...) accept a configurator, a
Consumer<PdoCrud<T>> resp. Consumer<PdoSearch<T>> invoked before the stage is shown. Together with
the hooks of the editor and finder, this is how a dialog is adapted to its use case:
| Where | Method | Effect |
|---|---|---|
PdoCrud |
setPdoList(list) |
Walk through a list of PDOs with the previous/next buttons, e.g. the result of a search. Also hides new, delete and find, as a modal CRUD does. |
addPdoEventHandler(type, handler) / addPdoEventFilter(...) |
React on the events of this dialog. | |
releasePdo(released) |
Ask the user to save, discard or cancel pending changes, and release the token lock. | |
PdoSearch |
setSingleSelectMode(true) |
Select exactly one PDO, usually in a modal dialog. |
setCreatePdoAllowed(boolean) |
Show or hide the new button. | |
setDoubleClickHandler(consumer) |
Replace the default action of a double-click in a non-modal dialog (opening the editor). | |
setFinderVisible(boolean) |
Hide the search criteria, e.g. to display a given result with setItems(list). Invoke it after setPdo. |
|
setNoDataMessage(text) |
The message shown if nothing was found. | |
showTree() / showTable() |
Toggle between the result table and a tree of the found PDOs. | |
filterResult() |
Narrow the displayed result by the text of the dialog's filter field, matched against the PDOs' normtext; a leading ! or - inverts the filter. |
|
PdoFinder |
setSearchRunningImmediately(true) |
Search as soon as the dialog opens. |
setSearchRunningInBackground(true) |
Run the query off the FX thread, for queries that may take a while. | |
setTreeShowingInitially(true) / setScrollingToEnd(true) |
Initial presentation of the result. | |
createTotals(items) / createTotalsTableView() |
A totals row below the result table, by default summing up all numeric columns. | |
PdoEditor |
isNewAllowed() / isRemoveAllowed() |
Enable the new and delete buttons; by default isEditAllowed(), for delete also the PDO's isRemovable(). |
validateForm() |
Checks beyond the model's validation, invoked within the save transaction before the PDO is persisted. | |
getValidationMappers() / getValidationPath() |
Map validation results from the server to the editor's controls. | |
requestInitialFocus() |
Put the focus on the first field when the dialog opens. |
Opening an existing PDO in a PdoCrud for editing requests its token lock,
if the entity provides one. If another user holds it, the editor switches to view-only and says so. The
token lock is released when the PDO is saved, discarded or the dialog is closed. save() runs the form
validation, the duplicate check of the unique domain key and the persist() in one transaction, and
keeps the editor open with the pending changes if any of them fails.
The toolbars hold a few features that are easy to miss:
- Shift + new in the CRUD dialog creates a deep copy of the current PDO instead of an empty one.
- Shift + find in the search dialog adds the new results to the displayed ones instead of replacing them.
- The search dialog's tree button toggles between the result table and a tree of the found PDOs; the CRUD dialog's tree button shows the tree of the current PDO. A secondary click on the CRUD's tree button shows the PDO's object ID.
- The security button of the CRUD dialog edits the security rules of a root entity, if the user is allowed to.
- The search dialog shows a filter field for entities with a normtext, see
filterResult()above. - The PDOs the user is not allowed to view are left out of the search result.
CRUD and search controllers are expensive to create, since each loads its FXML view. RdcUtilities
therefore caches and reuses them per PDO class, as long as the dialog is not showing. Opening a PDO
that is already being edited brings that dialog to the front instead of opening a second one.
Caching is off on servers and if the noRdcCache backend property
is set; switching between light and dark mode invalidates the caches, as does
RdcUtilities.getInstance().invalidateCaches(). Subclasses of RdcUtilities can decide per entity by
overriding isCrudCachingEnabled(pdo) and isSearchCachingEnabled(pdo).
Tables, trees and cells¶
RDC plugs PDOs into the
table and tree infrastructure
of tentackle-fx:
PdoTreeCellrenders a PDO in a tree with the text, tooltip and graphic supplied by itsGuiProvider, and supports drag and drop.PdoTableCellandPdoTreeTableCellare used for table columns holding a PDO and show itstoString(). All three show the PDO's context menu.PdoTreeItemlazily expands a tree by asking the provider for the child objects when they are needed for the first time. A recursive "expand all" stops at a PDO whose class already appears further up in the path (seeRdcFxUtilities), since expanding a deep object graph may take far too long.- The
tablepackage adds RDC-aware configuration (RdcTableConfiguration,RdcTableColumnConfiguration), a column-chooser/preferences popup (TablePopup) and printing (TablePrinter), see Table popup, export and printing below. - The
componentpackage contributes PDO-aware controls:RdcComboBoxandRdcChoiceBox, complete with thedelegate/buildcompanions that the fx delegate pattern prescribes. See PDO-bound components below.
Table popup, export and printing¶
Rdc.createTablePopup(table, preferencesSuffix, title) installs a
TablePopup as the context menu of a table
(or tree table) and all its columns. The search dialog's result table gets one automatically. It
offers:
- a column chooser to show or hide columns (
setColumnMenuEnabled(false)turns it off), - auto-adjusting the column widths to their content,
- printing,
- exporting all or only the selected rows to a spreadsheet or an XML file, which is opened in the associated application afterward,
- saving and loading the table preferences (column widths, order, visibility and sorting) in the user or system scope,
- for tree tables, expanding and collapsing the tree.
The exports and printing are implemented by
TableUtilities, a replaceable
@Service. Only the visible columns are exported, with the same text as displayed in the table. The
default spreadsheet format is CSV (RFC 4180); with
tentackle-fx-rdc-poi on the classpath,
it is an Excel file instead. The last file chosen is remembered per table in the JDK's user
preferences of the local machine.
TablePrinter prints the visible columns,
scaled down to the page width if necessary, with a title, the date and page numbers, and the totals
row of a table, if any. Because the rows per page depend on the actually rendered row heights, the pages
are prepared in a background task that hops to the FX thread for each page.
A progress dialog allows the user to cancel the job.
PDO-bound components¶
Any component bound to a PDO-valued
property — a text field showing a customer, a combo box selecting a currency — is enhanced by a
PdoComponentAddon, which the PDO
value translators attach automatically:
- the style class
tt-pdo-style, so PDO fields can be styled differently; - F2 (or edit in the context menu) opens the referenced PDO in a modal CRUD dialog — editable, unless the PDO is immutable — and shows the edited version afterwards;
- F3 (or search in the context menu) opens a modal search dialog for the component's type and puts the selected PDO into the field, if the component is changeable;
- drag and drop: a PDO can be dragged out of the field, and a PDO of a matching type dropped into it, e.g. from a table or tree.
Removing the style class tt-pdo-style from a component turns these features off. They need the
DomainContext as a binding property of the binder, which the PdoCrud and PdoSearch controllers
set up; for the F3 search, the PDO class must have a GuiProvider with a finder.
With tentackle-fx-rdc present, every ComboBox and ChoiceBox created from FXML is an
RdcComboBox resp. RdcChoiceBox (their builders replace the ones of tentackle-fx). When bound
to a PDO type, they fill their item list themselves: with all PDOs of that type in the binding's
domain context. The list is reloaded when the entity's table
modification count changes,
and a PDO edited via F2 replaces its outdated entry. By default, the PDOs are taken from the
cache via selectAllCached() if the entity has a
preloading cache; otherwise selectAll() is used and a warning recommends caching. A different
selection — only the active entries, say — is installed with setLoadAllPdosCallback(...), a callback
that receives a proxy PDO of the component's type and returns the items, e.g.
box.setLoadAllPdosCallback(proxy -> proxy.selectActive()) with an application-defined finder
selectActive().
The popup of such a combo box shows two columns, the PDO's short and long text, if it implements
ShortLongText; the
RdcShortLongTextCellFactory hides
the texts of PDOs the user is not allowed to view.
Value translators¶
The translate package supplies ready-made
value translators
for PDOs, so a PersistentDomainObject can appear in any FX control without
boilerplate:
PdoStringTranslator— PDO ⇄ display string. A text field shows the PDO's unique domain key. Entering a key looks the PDO up viafindByUniqueDomainKey; if there is none, a modal search dialog opens, preset with the input if the key class provides a staticlenientValueOf(String).PdoObservableListTranslator— a PDO list as an observable list, with the table configuration taken from the element type'sGuiProvider.PdoTreeItemTranslator/PdoCollectionTreeItemTranslator— PDOs as tree items.PdoComponentAddon— adds the PDO style, context menu, F2/F3 keys and drag and drop to the components they translate for, see PDO-bound components.
Context menus¶
Right-clicking a PDO in a table or tree raises a context menu assembled by a
PdoContextMenuFactory
(default
DefaultPdoContextMenuFactory).
Menu items are themselves discoverable services, so an application can add its own
without touching the framework:
- Items implement
PdoTableContextMenuItem,PdoTreeContextMenuItemorPdoTreeTableContextMenuItemand are declared with the matching@PdoTableContextMenuItemService/@PdoTreeContextMenuItemService/@PdoTreeTableContextMenuItemServiceannotation. - An item class must extend
MenuItemand provide a public constructor taking the cell (PdoTableCell,PdoTreeCellorPdoTreeTableCell). The annotation names the PDO class the item applies to —PersistentDomainObject.classfor all PDOs —, anordinalthat sorts the items, and an optionalgroup; groups are separated by a separator. Items applying to the PDO's own class win over items with the same ordinal registered for a superclass, which is how a built-in item is replaced for a particular entity. Invisible items are left out. - The built-in items live in the
contextmenupackage:
| Item | Ordinal | Tree | Table | Action |
|---|---|---|---|---|
ExpandItem |
100 | ✓ | Expand the node and all its children. | |
CollapseItem |
110 | ✓ | Collapse the node and all its children. | |
ViewItem |
140 | ✓ | ✓ | Open the PDO in a non-modal, read-only CRUD dialog. |
EditItem |
150 | ✓ | Edit the PDO in a modal CRUD dialog. |
View and edit are disabled unless the GuiProvider allows viewing or editing. There are no
built-in items for tree tables.
Events¶
A PdoCrud fires a PdoEvent for every persistence
operation it performs: CREATE and UPDATE after a save, DELETE after a delete, and READ when the
previous/next buttons load another PDO of the list (ANY matches them all). The events are
delivered through an EventListenerProxy to
the handlers and filters registered on that dialog via addPdoEventHandler / addPdoEventFilter;
they are neither broadcast to other windows nor tied to the database. The PdoSearch, for instance,
registers a filter on the CRUD it opens, to update the edited row and follow the list navigation.
Other windows showing the same data learn about changes through
modification tracking instead.
Background execution¶
Time-consuming persistence calls must never run on the JavaFX application thread. RDC provides
the bg(...) helpers on Rdc to run
work off-thread and post the result back:
Rdc.bg(node,
() -> invoice.reload(), // runs in the background
reloaded -> refreshView(reloaded), // runs on the FX thread
ex -> showError(ex)); // FX thread, on failure
This pattern keeps the UI responsive regardless of whether the PDO is served locally or remotely.
By default the task runs on the
ModificationTracker's
thread and session — no extra threads or sessions, but tasks are serialized and a
long one delays modification tracking. Applications that need several background
tasks in parallel replace RdcUtilities with a subclass of
RdcUtilitiesWithBackgroundPool,
which runs each task on its own pooled session. See
Background Execution for both models, the trade-offs
and the parallel-runner API.
Application lifecycle (app, login)¶
The app and login packages turn the building blocks above into a complete,
launchable application.
FxApplication— the base class for any Tentackle FX application.LoginApplication— usually the application that is launched first. It shows the login view and spawns the login handler; on success it hands over to the real application.DesktopApplication<C>— the main desktop application, parameterized by its main controller type.LoginFailedHandlerandLogoutEventHandler— pluggable handlers for the corresponding lifecycle events.
The login package implements the login flow:
Login (the controller),
LoginController (the
interface that lets an application substitute its own login view) and
Backends (add / edit /
remove the server backends the user can connect to).
Which credentials the view collects is decided by the authentication key of
backend.properties. With the default password method the user fills in the username and password
fields. With an interactive (single sign-on) method those fields are hidden and a sign-in button runs
the application's CredentialsProvider on a background thread —
FxCredentialsPrompt opens the system
browser and reports progress back into the view. See
Authentication.
Which backend the user connects to is decided by the backend key of backend.properties: with
@user or @system, the login view lets the user pick one of the backends stored in the user resp.
system preferences and edit them with the Backends
dialog (see Redirecting the Backend). A failed
login is handled by the LoginFailedHandler:
expired credentials are renewed once via the CredentialsProvider, a refused login may be retried twice
before the application terminates, and a client whose version does not match the server's is told so.
Applications replace the login view by overriding LoginApplication.loadLoginController(), and the
handler via DesktopApplication.createLoginFailedHandler(...).
A minimal startup therefore looks like:
launch LoginApplication
└─ user picks a Backend and authenticates (login package)
└─ on success → DesktopApplication starts with the main controller
└─ controllers use Rdc / GuiProvider to search, view and edit PDOs
Security and administration¶
- The
securitypackage provides a UI for editing the PDO security rules enforced by the persistence layer:SecurityEditor,SecurityRulesView, and aSecurityDialogFactory(defaultDefaultSecurityDialogFactory). Security thus participates in the same per-aggregate model described in the PDO documentation. - The
adminpackage contains operational views, see Session administration below.
Session administration¶
A server knows every client session connected to it. The
AdminExtension, a
remote session extension implemented by the persistence layer's AdminExtensionAdapter, makes this
available to clients:
getSessions()lists the sessions asSessionData: user, application, since when, client version, host, locale, time zone, JVM and OS info, session number and group.logout(userId, sessionGroupId, applicationName, applicationId)asks a client to log out: it sends aLogoutEventthrough the user's modification tracker session, which the client handles like a regular logout (seeLogoutEventHandler).kill(...)closes the matching sessions on the server at once and returns their number.
0 or null for the group, application name or application id match any. The client obtains the
extension with ((PdoRemoteSession) session.getRemoteSession()).getExtension(context,
AdminExtension.class), so it is only available in a three-tier setup, and the server checks the
execute permission on AdminExtensionAdapter for every call — grant it to administrators via the
security rules.
SessionsView is a ready-made view on top of
it: a table of the live sessions with buttons to refresh, log out and kill the selected ones.
Logging out or killing a selected session applies to all sessions of the same session group and user
as well, e.g. the modification tracker's session of that client. The view's security button edits
the rules for AdminExtension. Override isKillSessionAllowed() to hide the kill button.
Annotation processors (apt)¶
To keep service discovery working in both modular (JPMS) and classpath
deployments, the apt package provides validators to ensure the contracts.
GuiProviderServiceAnnotationProcessor— for@GuiProviderService: the class must implementGuiProviderand provide a constructor taking the PDO.PdoTableContextMenuItemServiceAnnotationProcessor/PdoTreeContextMenuItemServiceAnnotationProcessor— for the context-menu item service annotations: the class must implement the item interface and provide a constructor taking the cell.@PdoTreeTableContextMenuItemServiceis not checked at compile time.
How It Fits Together¶
A typical "open an entity for editing" flow ties the pieces together:
- The user triggers an action (menu, button, double-click in a table).
- Calling code asks
Rdcfor the operation, e.g.Rdc.displayCrudStage(pdo, true, owner). RdcFactoryresolves theGuiProviderfor the PDO's type viaGuiProviderFactory.- The provider creates a
PdoEditor(an FX controller fromtentackle-fx), whichPdoCrudwraps with toolbar, navigation and persistence actions. - The editor's controls are bound to the PDO's attributes; edits flow through the binding layer.
- On save,
PdoCrudvalidates and persists the PDO within a single transaction; on completion it fires aPdoEventto the handlers registered on it — e.g. the search dialog it was opened from, which updates its result row. Lengthy work — a slow search, printing — is moved off the FX thread withRdc.bg(...).
The application only had to supply an editor view (or even use a generated default)
and a GuiProvider; everything else came from the framework.
Package Reference¶
| Package | Contents |
|---|---|
org.tentackle.fx.rdc |
Core: Rdc, RdcFactory, GuiProvider, CRUD controllers, table/tree cells, events, utilities. |
org.tentackle.fx.rdc.app |
Application lifecycle: FxApplication, LoginApplication, DesktopApplication, lifecycle handlers. |
org.tentackle.fx.rdc.login |
Login view, controller and backend management. |
org.tentackle.fx.rdc.crud |
The generic PdoCrud create/read/update/delete controller. |
org.tentackle.fx.rdc.search |
PdoSearch and the default finder. |
org.tentackle.fx.rdc.table |
RDC table configuration, table popup, export, printing, table utilities. |
org.tentackle.fx.rdc.component(.delegate/.build) |
PDO-aware combo/choice boxes with their delegates and builders. |
org.tentackle.fx.rdc.translate |
Predefined PDO value translators. |
org.tentackle.fx.rdc.contextmenu |
Built-in context-menu items (edit, view, expand, collapse). |
org.tentackle.fx.rdc.security |
Security-rules editing UI. |
org.tentackle.fx.rdc.admin |
Administrative views (e.g. live sessions). |
org.tentackle.fx.rdc.apt |
Annotation processors verifying the service annotations at compile time. |
org.tentackle.fx.rdc.service |
The module hook. |