Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1605 +/- ##
============================================
+ Coverage 61.89% 61.92% +0.02%
- Complexity 11244 11270 +26
============================================
Files 1247 1247
Lines 48367 48435 +68
Branches 6788 6801 +13
============================================
+ Hits 29937 29993 +56
- Misses 15725 15731 +6
- Partials 2705 2711 +6 🚀 New features to boost your workflow:
|
|
@reiern70 can you provide example scenario(s) where you find a need to have multiple models for a component? |
Components with more than one modelWicket 11 lets a component register models beside its default model. The framework then 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 1. A component with two modelsBeforeA component has exactly one default model — the one passed to 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 After
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.
}
2. Models the component creates itselfThe most common case for several models: the component builds its own loadable models from its Beforepublic 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();
}
}Afterpublic 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 3. Models the component does not need a field forWhen a model is only handed to a child and never touched again, there is nothing to assign it to. Beforepublic 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();
}
}AfterPass them straight to the constructor: 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 4. Replacing a model in a setterA model field with a setter has to detach the model it drops — otherwise the old model is simply Beforepublic 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
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 knowBeforeBoth classes have an 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();
}
}AfterAdditional models are tracked as a set, not by index. A subclass registers its own models without 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.
|
| 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.
659678e to
d539aed
Compare
|
It's been a long time since I looked at a PR for Wicket, but I've been getting back into things over the past couple years. I feel a bit "on the fence" on this PR. I can see it both ways. Arguably, you could say that a component should have one IModel and that cases where you have multiple models are usually a case where things could be decomposed better. For example, you could have a composite IModel that detaches its sub-models or in some cases you could simply break up the component into sub-panels. But there are cases where this PR addresses a real flaw, including one that's already in Wicket. For example, the multi-select list choice component quite legitimately has two unrelated models (the options model and the selection model). What we wound up doing 15-20 years ago with this is to have a component have one primary or "default" model (that's why it's getDefaultModelObject() instead of getModelObject() if I remember correctly) and then your component can have more models that you manage yourself. If this PR gets approved, it seems like the multi-select choice component is a poster child for the issue this PR addresses and that it would benefit by adopting this API. My first worry when I saw the PR title was negative because I didn't want the IModel field on Component to become a list, but I think this PR recognizes that this is an unusual case and using metadata seems one good solution to keep components slim while implementing support for detaching multiple models. Another one would be be creating a composite model that absorbs and manages the primary and additional models. That might be worth prototyping, actually to see if it is cleaner/simpler. I can't yet see a reason to vote "no" on this, but it would be good to do some thinking about what this does to Wicket overall in terms of how people code in it. It's a pretty deep change, so it would be good to get a lot of eyes on this, but I can't see a good reason to say "no" at first glance. If approved, I'd like to see any core components in wicket that already have multiple models adopt the API in a follow-on PR. |
Hi... thanks for your feedback and thanks for Wicket! and welcome back!
|
A component has a single model that the framework detaches at the end of
each request. A component using further models has to detach them itself in
onDetach(), detachModel() or detachModels(); when it forgets, a
LoadableDetachableModel stays loaded and is serialized with the page.
Component can now track additional models next to the default model:
- addAdditionalModel(M) registers a model and returns it, typed as given, so
it can be assigned in the same statement:
foo = addAdditionalModel(new FooModel()). Registering a model twice has no
effect.
- replaceAdditionalModel(IModel, M) detaches and unregisters the previous
model and registers the given one, unless both are the same, and returns
the given model; it is meant for setters.
- removeAdditionalModel(M) detaches, unregisters and returns a model.
- getModels() returns the default model, if any, followed by the additional
models, without triggering model inheritance.
- detachModels() detaches the additional models, including the model inside
a wrapper, as detachModel() does for the default model.
- Component(String, IModel, IModel...) registers the given additional
models; MarkupContainer, WebMarkupContainer, WebComponent, Panel and
GenericPanel get the same constructor. The existing (String, IModel)
constructors stay; a (String, IModel...) overload was avoided because it
would make new X("id", null) ambiguous.
Additional models are registered as given and not wrapped, so the methods
can return the model itself; a component using an IComponentAssignedModel
registers wrap(model). They are tracked without an index, so a subclass does
not need to know which models its superclasses use.
The models are kept as component meta data, which costs a component roughly
50 to 80 bytes more than detaching the same models by hand, whatever their
number: a meta data entry, the array holding the models, and the state
holder a component needs once it has more than one kind of state. That is a
good trade for a component used a few dozen times on a page and a bad one
for a component rendered in the thousands, so the components shipped with
Wicket are left as they are and keep detaching their models themselves. The
javadoc of addAdditionalModel and the user guide say so, with the numbers.
Detaching an IWrapModel whose wrapped model is null no longer throws.
The user guide section on components with more than one model describes the
new methods.
The guide section on components with more than one model now shows each case
before and after: a component with two models, models the component builds
itself, models it keeps no reference to, a setter, a subclass registering
beside its superclass, a model that has to be wrapped, and getModels(). The
passage telling the reader to detach by hand is kept, because that is still
the right answer for a component rendered in the thousands.
wicket-examples gains a component reference page, MultipleModelsPage, with a
CustomerCardPanel that uses four models and overrides no onDetach at all. It
lists its own models through getModels(), counts how often the orders model
is loaded so that one load per request is visible in the page, and replaces
that model through a setter, which exercises replaceAdditionalModel. Its
texts come from a resource bundle, through wicket:message in the markup and
StringResourceModel in the code, the order status through a key built from
the model.
wicket-benchmarks, wicket-coverage, wicket-extensions-tester and wicket-migration are modules of the reactor but appeared neither in the directory tree nor in the list of projects, so a reader of the README got an incomplete picture of the distribution. They are now in both, with a line each saying what they are for. The tree also spelled wicket-objectsizeof-agent with a doubled s, which is not the name of the module.
The javadoc of addAdditionalModel and the user guide put the price of letting Component detach a component's extra models at roughly 50 to 80 bytes per component, a figure no benchmark in the tree could produce: ComponentFootprint measures state shapes built by decorating a plain WebMarkupContainer, and the approach it has to be compared against is a subclass with model fields and a hand written onDetach, which a Shape cannot express. Trait.METADATA also stores a String under a MetaDataKey rather than the IModel[] the feature creates, so MODEL_METADATA was only ever a proxy for it. AdditionalModelsShapes holds the three ways a component can carry the same extra models: in fields detached by hand, in fields registered with addAdditionalModel, and registered through the constructor with no fields at all. Every variant carries the same default model and the same extra models, so a difference between two of them is the bookkeeping alone. AdditionalModelsFootprint reports retained heap and serialized bytes for one, two and three extra models, against the same component carrying only a default model. AdditionalModelsBenchmark reports time and allocation for constructing such a component and for the whole construct-and-detach cycle, which is where registering does its extra work. run-model-benchmarks.sh runs all of it. It copies the dependencies into the module rather than pointing the classpath at ~/.m2, and puts the target/classes directories first, so a run measures the working copy and not whatever was last installed. The measurements confirm the documented figure and sharpen it: 72 to 80 bytes of retained heap per component, 56 to 64 with compact object headers, and near flat in the number of models because only the IModel[] grows. Serialized size is not flat, rising about 5 bytes per model, and construction is the cost the prose does not mention at all: 80% slower with three extra models, 128 bytes more allocated per component.
The benchmarks report numbers; nothing in the tree said what they came out as, so the figure quoted in the javadoc and the user guide could not be checked without re-running them. MODEL_BENCH_MARK.md holds the results of one run: retained heap and serialized bytes per component for one, two and three extra models, with and without compact object headers, the JOL breakdown of where the bytes go, and time and allocation for construction and for the construct-and-detach cycle. It names the JVM, the heap and the JMH settings it was produced with, so a later run can be compared against it or dismissed as measuring something else. The conclusions it draws: the documented 50 to 80 bytes is right and slightly conservative, the cost is near flat in the number of models for heap but not for serialized size, and construction is 80% slower with three extra models, which is the number that decides whether a component rendered in the thousands can afford the convenience.
d539aed to
8af129a
Compare
There was a problem hiding this comment.
@papegaaij This document contains the numbers of the new benchmarks
A component has a single model that the framework detaches at the end of each request. A component using further models has to detach them itself in onDetach(), detachModel() or detachModels(); when it forgets, a LoadableDetachableModel stays loaded and is serialized with the page.
Component can now track additional models next to the default model:
Additional models are registered as given and not wrapped, so the methods can return the model itself; a component using an IComponentAssignedModel registers wrap(model). They are tracked without an index, so a subclass does not need to know which models its superclasses use.
The models are kept as component meta data, which costs a component roughly 50 to 80 bytes more than detaching the same models by hand, whatever their number: a meta data entry, the array holding the models, and the state holder a component needs once it has more than one kind of state. That is a good trade for a component used a few dozen times on a page and a bad one for a component rendered in the thousands, so the components shipped with Wicket are left as they are and keep detaching their models themselves. The javadoc of addAdditionalModel and the user guide say so, with the numbers.
Detaching an IWrapModel whose wrapped model is null no longer throws.
The user guide section on components with more than one model describes the new methods.