The model layer is a complete, UI-independent application: data retrieval, editing, validation, selection, master-detail coordination and all associated state live here, exposed through the framework’s reactive classes. The UI layer renders models — it does not extend them with logic. Nothing in the model layer depends on a UI toolkit, which has two practical consequences: application logic is unit-testable without showing a window, and the same models can drive different client technologies.
1. The pieces
An EntityModel is the composition root for a single entity type: an edit model (wrapping the editor — the write path), usually a table model (with its query model — the read path), and any detail models.
SwingEntityModel customerModel = new SwingEntityModel(Customer.TYPE, connectionProvider);
SwingEntityModel invoiceModel = new SwingEntityModel(Invoice.TYPE, connectionProvider);
// Establish master-detail relationship
customerModel.detail().add(invoiceModel);
Master-detail relationships are expressed by linking models, to arbitrary depth — a detail model’s query condition tracks its master’s selection automatically:
// Three-level hierarchy
SwingEntityModel customerModel = new SwingEntityModel(Customer.TYPE, connectionProvider);
SwingEntityModel invoiceModel = new SwingEntityModel(Invoice.TYPE, connectionProvider);
SwingEntityModel invoiceLineModel = new SwingEntityModel(InvoiceLine.TYPE, connectionProvider);
customerModel.detail().add(invoiceModel);
invoiceModel.detail().add(invoiceLineModel);
// Selection cascades down the hierarchy
Entity customer = getCustomer(connectionProvider);
customerModel.tableModel().selection().item().set(customer);
// Invoices for selected customer are loaded
Entity invoice = invoiceModel.tableModel().items().included().get(0);
invoiceModel.tableModel().selection().item().set(invoice);
// Invoice lines for selected invoice are loaded
See Model linking for the linking configuration — what happens on selection, insert, update and delete.
2. The reactive fabric
Everything a model knows is exposed as an observable Value, State or Event — which is all a UI needs to render it, and all application logic needs to react to it:
SwingEntityModel customerModel = new SwingEntityModel(Customer.TYPE, connectionProvider);
SwingEntityEditModel editModel = customerModel.editModel();
SwingEntityTableModel tableModel = customerModel.tableModel();
// Edit model states
State updateEnabled = editModel.editor().settings().updateEnabled();
State updateMultipleEnabled = editModel.editor().settings().updateMultipleEnabled();
ObservableState modified = editModel.editor().entity().modified();
// Table model states
ObservableState refreshing = tableModel.items().refresher().active();
ObservableState hasSelection = tableModel.selection().empty().not();
// Combine states
ObservableState canDelete = State.and(hasSelection, refreshing.not());
Entity values are observable per attribute, so values can be bound — to input components, or across models:
SwingEntityModel trackModel = new SwingEntityModel(Track.TYPE, connectionProvider);
SwingEntityEditModel editModel = trackModel.editModel();
// Bind edit model value to UI state
EditorValue<BigDecimal> priceValue = editModel.editor().value(Track.UNITPRICE);
ObservableState priceValid = editModel.editor().value(Track.UNITPRICE).valid();
// React to value changes
priceValue.addConsumer(this::updateTotalPrice);
// React to value edits
priceValue.edited().addConsumer(newPrice -> System.out.println("Price: " + newPrice));
priceValid.when(false)
.addListener(() -> System.out.println("Invalid price: " + priceValue.get()));
3. Where application logic belongs
Logic belongs in the models — reacting to edits, persistence events and selection there means it works the same regardless of which UI (or test) drives it:
SwingEntityModel invoiceLineModel = new SwingEntityModel(InvoiceLine.TYPE, connectionProvider);
// Update summary when details change
PersistEvents events = invoiceLineModel.editor().events();
events.after().insert().addConsumer(entities -> updateInvoiceTotal());
events.after().update().addConsumer(entities -> updateInvoiceTotal());
events.after().delete().addConsumer(entities -> updateInvoiceTotal());
The chinook demo’s models are the reference examples: value dependencies and custom persistence in InvoiceLineEditModel, value propagation in InvoiceEditModel, selection-scoped operations in TrackTableModel — each documented in the chapters that follow.
|
Note
|
Since models are UI-free, they are constructed and exercised directly in unit tests — create the model with a test connection provider, edit, insert, assert. |