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]

Reply via email to