reiern70 commented on PR #1605:
URL: https://github.com/apache/wicket/pull/1605#issuecomment-5732511137
> @reiern70 can you provide example scenario(s) where you find a need to
have multiple models for a component?
# Components with more than one model
Wicket 11 lets a component register models **beside** its default model. The
framework then
detaches them for you, at the end of every request, together with the
default model.
The short version: **you no longer have to care about detaching. The
component detaches itself.**
Every section below is the same code twice — *before*, as you write it
today, and *after*, with
the new API.
---
## 1. A component with two models
### Before
A component has exactly one *default* model — the one passed to `super(id,
model)` — and the
framework detaches that one. Every other model kept in a field is the
developer's problem:
```java
public class CustomerCardPanel extends GenericPanel<Customer>
{
private final IModel<List<Order>> ordersModel;
private final IModel<Address> addressModel;
public CustomerCardPanel(String id, IModel<Customer> customer,
IModel<List<Order>> orders, IModel<Address> address)
{
super(id, customer);
// Just fields. Nothing detaches these two.
this.ordersModel = orders;
this.addressModel = address;
}
@Override
protected void onDetach()
{
// Forget one of these lines and the mistake is silent: the
// LoadableDetachableModel stays loaded, its entities are
dragged into the
// page store, and the page grows for as long as it lives in
the session.
ordersModel.detach();
addressModel.detach();
super.onDetach();
}
}
```
Nothing tells you when you get this wrong. The page still renders. It is
only bigger, staler and
slower than it should be — and the model still holds last request's data.
### After
`addAdditionalModel(model)` registers a model and **returns it, typed as it
was given**, so the
registration happens in the same statement as the assignment. The field
stays `final`, and
`onDetach()` disappears:
```java
public class CustomerCardPanel extends GenericPanel<Customer>
{
private final IModel<List<Order>> ordersModel;
private final IModel<Address> addressModel;
public CustomerCardPanel(String id, IModel<Customer> customer,
IModel<List<Order>> orders, IModel<Address> address)
{
super(id, customer);
// Registered, not just assigned: both are detached with the
default model.
this.ordersModel = addAdditionalModel(orders);
this.addressModel = addAdditionalModel(address);
}
// No onDetach(). There is nothing left to detach by hand.
}
```
`addAdditionalModel` accepts `null` (it registers nothing and returns
`null`), and registering the
same model twice has no effect — so a component that is unsure whether a
model was already
registered can simply register it again.
---
## 2. Models the component creates itself
The most common case for several models: the component builds its own
loadable models from its
default model.
### Before
```java
public class CustomerCardPanel extends GenericPanel<Customer>
{
private final IModel<List<Order>> ordersModel;
private final IModel<Integer> unpaidCountModel;
public CustomerCardPanel(String id, IModel<Customer> customer,
OrderService orders)
{
super(id, customer);
this.ordersModel = LoadableDetachableModel.of(
() -> orders.findByCustomer(getModelObject()));
this.unpaidCountModel = LoadableDetachableModel.of(
() ->
(int)ordersModel.getObject().stream().filter(Order::isUnpaid).count());
}
@Override
protected void onDetach()
{
// Two models created here, two lines to keep in sync with the
fields above.
// Add a third model next year and this method has to be
remembered.
ordersModel.detach();
unpaidCountModel.detach();
super.onDetach();
}
}
```
### After
```java
public class CustomerCardPanel extends GenericPanel<Customer>
{
private final IModel<List<Order>> ordersModel;
private final IModel<Integer> unpaidCountModel;
public CustomerCardPanel(String id, IModel<Customer> customer,
OrderService orders)
{
super(id, customer);
// A model per piece of derived state, each loaded at most once
per request
// and dropped again when the request ends.
this.ordersModel = addAdditionalModel(
LoadableDetachableModel.of(() ->
orders.findByCustomer(getModelObject())));
this.unpaidCountModel = addAdditionalModel(
LoadableDetachableModel.of(() ->
(int)ordersModel.getObject().stream()
.filter(Order::isUnpaid)
.count()));
}
}
```
Note what did *not* have to be written: no `onDetach()`, no null checks, no
bookkeeping about
which of the two models is loaded. `unpaidCountModel` reads through
`ordersModel`, so the orders
are fetched once and both models drop their reference at the end of the
request.
---
## 3. Models the component does not need a field for
When a model is only handed to a child and never touched again, there is
nothing to assign it to.
### Before
```java
public class InvoiceHeaderPanel extends GenericPanel<Invoice>
{
// Fields that exist for one reason only: so that onDetach() can reach
them.
private final IModel<Company> issuerModel;
private final IModel<Customer> recipientModel;
public InvoiceHeaderPanel(String id, IModel<Invoice> invoice,
IModel<Company> issuer, IModel<Customer> recipient)
{
super(id, invoice);
this.issuerModel = issuer;
this.recipientModel = recipient;
add(new CompanyPanel("issuer", issuer));
add(new CustomerPanel("recipient", recipient));
}
@Override
protected void onDetach()
{
issuerModel.detach();
recipientModel.detach();
super.onDetach();
}
}
```
### After
Pass them straight to the constructor: `Component(String, IModel,
IModel...)` registers every
additional model it is given. `MarkupContainer`, `WebComponent`,
`WebMarkupContainer`, `Panel` and
`GenericPanel` all have the same constructor.
```java
public class InvoiceHeaderPanel extends GenericPanel<Invoice>
{
public InvoiceHeaderPanel(String id, IModel<Invoice> invoice,
IModel<Company> issuer, IModel<Customer> recipient)
{
// invoice is the default model; issuer and recipient are
registered as
// additional models and detached along with it.
super(id, invoice, issuer, recipient);
add(new CompanyPanel("issuer", issuer));
add(new CustomerPanel("recipient", recipient));
}
}
```
Two fields, two detach calls and the whole `onDetach()` are gone. The child
components hold these
models too, but a child only detaches *its own* default model — which is the
very same instance,
so detaching it twice is harmless, and registering it here is what
guarantees it happens even when
the children are not rendered.
---
## 4. Replacing a model in a setter
A model field with a setter has to detach the model it drops — otherwise the
old model is simply
leaked, and the new one may never be detached at all.
### Before
```java
public void setAddressModel(IModel<Address> addressModel)
{
// Easy to get wrong in both directions: forget the detach and the old
model
// leaks; detach unconditionally and you detach a model that is still
in use
// when the same instance is set twice.
if (this.addressModel != null && this.addressModel != addressModel)
{
this.addressModel.detach();
}
this.addressModel = addressModel;
}
```
### After
`replaceAdditionalModel(previous, model)` does both halves: it detaches and
unregisters
`previous`, registers `model`, and returns `model`.
```java
public void setAddressModel(IModel<Address> addressModel)
{
// Detaches and unregisters the previous model, registers the new one.
// Passing the same model twice is a no-op, so nothing is detached
needlessly.
this.addressModel = replaceAdditionalModel(this.addressModel,
addressModel);
}
public void clearAddressModel()
{
// removeAdditionalModel detaches and unregisters, and returns what it
was given.
removeAdditionalModel(this.addressModel);
this.addressModel = null;
}
```
---
## 5. Inheritance: a subclass does not need to know
### Before
Both classes have an `onDetach()`, and the subclass has to remember
`super.onDetach()` — miss it
and the superclass' models silently stop being detached:
```java
public class BaseCardPanel<T> extends GenericPanel<T>
{
protected final IModel<Branding> brandingModel;
public BaseCardPanel(String id, IModel<T> model)
{
super(id, model);
this.brandingModel =
LoadableDetachableModel.of(BrandingService::current);
}
@Override
protected void onDetach()
{
brandingModel.detach();
super.onDetach();
}
}
public class CustomerCardPanel extends BaseCardPanel<Customer>
{
private final IModel<List<Order>> ordersModel;
// ... constructor ...
@Override
protected void onDetach()
{
ordersModel.detach();
// Forget this line and brandingModel is never detached again.
super.onDetach();
}
}
```
### After
Additional models are tracked as a set, not by index. A subclass registers
its own models without
knowing, or caring, how many its superclass already registered — and nothing
breaks when the
superclass later adds one:
```java
public class BaseCardPanel<T> extends GenericPanel<T>
{
protected final IModel<Branding> brandingModel;
public BaseCardPanel(String id, IModel<T> model)
{
super(id, model);
this.brandingModel = addAdditionalModel(
LoadableDetachableModel.of(BrandingService::current));
}
}
public class CustomerCardPanel extends BaseCardPanel<Customer>
{
private final IModel<List<Order>> ordersModel;
public CustomerCardPanel(String id, IModel<Customer> customer,
OrderService orders)
{
super(id, customer);
// Registered next to the superclass' model. No index, no super
call to remember,
// no coordination between the two classes.
this.ordersModel = addAdditionalModel(
LoadableDetachableModel.of(() ->
orders.findByCustomer(getModelObject())));
}
}
```
---
## 6. `IComponentAssignedModel`: wrap it yourself
The default model is wrapped for the component behind your back — that is how
`CompoundPropertyModel` learns the component it belongs to. An additional
model is registered
**exactly as given**, so that the method can hand it back to you unchanged.
### Before
```java
// The model was never bound to this component, so the bundle is resolved
// against whatever component happens to render the string.
this.titleModel = new ResourceModel("customer.card.title");
```
### After
If the model is an `IComponentAssignedModel` — `ResourceModel`,
`StringResourceModel`,
`CompoundPropertyModel`, `PropertyModel` on a compound parent — wrap it
yourself:
```java
// wrap() binds the ResourceModel to this component, so the bundle is
resolved
// relative to this panel. The wrapper is what gets registered and detached,
// and the model inside it is detached with it.
this.titleModel = addAdditionalModel(wrap(new
ResourceModel("customer.card.title")));
```
For a plain model — `Model`, `LoadableDetachableModel`, a lambda model —
there is nothing to wrap
and nothing to think about.
---
## 7. Seeing what a component holds
### Before
There was no way to ask. The additional models were private fields of the
component, so a test
could only assert on their effects.
### After
`getModels()` returns the default model first (when there is one) followed
by the additional
models, in registration order. It does **not** trigger model inheritance, so
calling it never
creates a model as a side effect — which makes it safe for debugging, tests
and tooling:
```java
// e.g. in a test: assert that the panel really registered everything it
should
Collection<IModel<?>> models = panel.getModels();
assertEquals(3, models.size());
```
---
## 8. When *not* to use this
Registering is a convenience with a price. The models are kept as component
meta data, which
costs roughly **50 to 80 bytes per component** more than detaching the same
models by hand —
whatever the number of models, since it pays for the meta data entry, the
array and the
component's state holder.
- A panel used a few dozen times on a page: register, and never think about
detaching again.
- A component rendered in the thousands — a cell inside a large `DataTable`,
an item of a long
`ListView`: those bytes are multiplied by the instance count. Keep the
*before* version and
detach by hand in `onDetach()`.
This is why the components shipped with Wicket keep detaching their own
models: they have to
assume the second case. Application components are almost always the first.
---
## API summary
| Method | What it does |
| --- | --- |
| `addAdditionalModel(M model)` | Registers `model`, returns it typed as
given. `null` and double registration are no-ops. |
| `replaceAdditionalModel(IModel<?> previous, M model)` | Detaches and
unregisters `previous`, registers and returns `model`. Same model twice is a
no-op. |
| `removeAdditionalModel(M model)` | Detaches, unregisters and returns
`model`. |
| `getModels()` | Default model (if any) then the additional models, in
registration order. Never initializes a model. |
| `Component(String id, IModel<?> model, IModel<?>... additionalModels)` |
Default model plus additional models to register. |
`addAdditionalModel`, `replaceAdditionalModel` and `removeAdditionalModel`
are `protected final` —
a component manages its own models. `getModels()` is `public final`.
All of it is `@since 11.0.0`.
---
## Running example
A working version of this lives in the component reference of
`wicket-examples`:
`MultipleModelsPage` in `org.apache.wicket.examples.compref`, reachable from
the reference index.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]