This is an automated email from the ASF dual-hosted git repository. reiern70 pushed a commit to branch reiern70/support-multiple-models in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 659678ed2bd3fe32a30cd4dd7e1e748bfad68862 Author: reiern70 <[email protected]> AuthorDate: Thu Sep 17 16:14:36 2026 -0500 Let an application component register models beside its default model 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. --- .../org/apache/wicket/ComponentModelsTest.java | 281 +++++++++++++++++++++ .../src/main/java/org/apache/wicket/Component.java | 268 +++++++++++++++++++- .../java/org/apache/wicket/MarkupContainer.java | 10 + .../apache/wicket/markup/html/WebComponent.java | 10 + .../wicket/markup/html/WebMarkupContainer.java | 10 + .../wicket/markup/html/panel/GenericPanel.java | 16 ++ .../org/apache/wicket/markup/html/panel/Panel.java | 9 + .../main/asciidoc/modelsforms/modelsforms_8.adoc | 47 +++- 8 files changed, 633 insertions(+), 18 deletions(-) diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/ComponentModelsTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/ComponentModelsTest.java new file mode 100644 index 0000000000..6e374767fb --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/ComponentModelsTest.java @@ -0,0 +1,281 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.Serializable; +import java.util.Arrays; + +import org.apache.wicket.markup.ComponentTag; +import org.apache.wicket.markup.html.WebComponent; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.model.CompoundPropertyModel; +import org.apache.wicket.model.IComponentAssignedModel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.IWrapModel; +import org.apache.wicket.model.LoadableDetachableModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +/** + * Tests a component with models beside its default model. + */ +class ComponentModelsTest extends WicketTestCase +{ + @Test + void constructorAddsAdditionalModels() + { + IModel<String> first = Model.of("first"); + IModel<String> second = Model.of("second"); + IModel<String> third = Model.of("third"); + + TestComponent component = new TestComponent("c", first, second, null, third); + + assertSame(first, component.getDefaultModel()); + assertEquals(Arrays.asList(first, second, third), component.getModels()); + } + + @Test + void defaultModelIsStillInheritedWithAdditionalModels() + { + WebMarkupContainer parent = new WebMarkupContainer("parent", + new CompoundPropertyModel<>(new Bean())); + IModel<String> additional = Model.of("additional"); + TestComponent child = new TestComponent("name", null, additional); + parent.add(child); + + assertEquals(Arrays.asList(additional), child.getModels()); + assertEquals("bean", child.getDefaultModelObject()); + } + + @Test + void allModelsAreDetachedAtTheEndOfTheRequest() + { + CountingModel defaultModel = new CountingModel(); + CountingModel additional = new CountingModel(); + TestComponent component = new TestComponent("c", defaultModel); + component.additional = component.addAdditionalModel(additional); + + tester.startComponentInPage(component); + + assertEquals(1, defaultModel.loads); + assertEquals(1, additional.loads); + assertFalse(defaultModel.isAttached()); + assertFalse(additional.isAttached()); + } + + @Test + void addingTwiceAddsOnce() + { + IModel<String> model = Model.of("model"); + TestComponent component = new TestComponent("c", null); + + assertSame(model, component.addAdditionalModel(model)); + assertSame(model, component.addAdditionalModel(model)); + + assertEquals(Arrays.asList(model), component.getModels()); + } + + @Test + void addingNullAddsNothing() + { + TestComponent component = new TestComponent("c", null); + + assertNull(component.addAdditionalModel(null)); + + assertTrue(component.getModels().isEmpty()); + } + + @Test + void registeringReturnsTheGivenModelWithItsType() + { + TestComponent component = new TestComponent("c", null); + + CountingModel model = component.addAdditionalModel(new CountingModel()); + + assertEquals(Arrays.asList(model), component.getModels()); + assertSame(model, component.removeAdditionalModel(model)); + assertTrue(component.getModels().isEmpty()); + } + + @Test + void registeredModelIsNotWrapped() + { + AssignedModel assigned = new AssignedModel(); + TestComponent component = new TestComponent("c", null); + + assertSame(assigned, component.addAdditionalModel(assigned)); + + assertNull(assigned.component); + assertEquals(Arrays.asList(assigned), component.getModels()); + } + + @Test + void wrappedModelIsRegisteredAsTheWrapper() + { + AssignedModel assigned = new AssignedModel(); + TestComponent component = new TestComponent("c", null); + + IModel<String> wrapped = component.addAdditionalModel(component.wrap(assigned)); + + assertSame(component, assigned.component); + assertSame(assigned, component.addAdditionalModel(assigned)); + assertEquals(Arrays.asList(wrapped), component.getModels()); + + assertSame(assigned, component.removeAdditionalModel(assigned)); + assertTrue(component.getModels().isEmpty()); + } + + @Test + void replaceDetachesAndRemovesThePreviousModel() + { + CountingModel previous = new CountingModel(); + IModel<String> other = Model.of("other"); + TestComponent component = new TestComponent("c", null); + component.addAdditionalModel(previous); + component.addAdditionalModel(other); + previous.getObject(); + + IModel<String> next = Model.of("next"); + assertSame(next, component.replaceAdditionalModel(previous, next)); + + assertFalse(previous.isAttached()); + assertEquals(Arrays.asList(other, next), component.getModels()); + } + + @Test + void replaceWithTheSameModelKeepsIt() + { + TestComponent component = new TestComponent("c", null); + CountingModel model = component.addAdditionalModel(new CountingModel()); + model.getObject(); + + assertSame(model, component.replaceAdditionalModel(model, model)); + + assertTrue(model.isAttached()); + assertEquals(Arrays.asList(model), component.getModels()); + } + + @Test + void replaceWithNullRemoves() + { + IModel<String> model = Model.of("model"); + TestComponent component = new TestComponent("c", null); + component.addAdditionalModel(model); + + assertNull(component.replaceAdditionalModel(model, null)); + + assertTrue(component.getModels().isEmpty()); + } + + @Test + void removingAModelThatWasNotAddedHasNoEffect() + { + IModel<String> model = Model.of("model"); + TestComponent component = new TestComponent("c", null); + component.addAdditionalModel(model); + + component.removeAdditionalModel(Model.of("other")); + component.removeAdditionalModel(null); + + assertEquals(Arrays.asList(model), component.getModels()); + } + + private static class TestComponent extends WebComponent + { + private static final long serialVersionUID = 1L; + + private IModel<?> additional; + + TestComponent(String id, IModel<?> model, IModel<?>... additionalModels) + { + super(id, model, additionalModels); + } + + @Override + protected void onComponentTag(ComponentTag tag) + { + super.onComponentTag(tag); + + getDefaultModelObject(); + additional.getObject(); + } + } + + private static class AssignedModel implements IComponentAssignedModel<String> + { + private static final long serialVersionUID = 1L; + + private Component component; + + @Override + public String getObject() + { + return null; + } + + @Override + public IWrapModel<String> wrapOnAssignment(Component component) + { + this.component = component; + return new IWrapModel<>() + { + private static final long serialVersionUID = 1L; + + @Override + public IModel<?> getWrappedModel() + { + return AssignedModel.this; + } + + @Override + public String getObject() + { + return null; + } + }; + } + } + + private static class CountingModel extends LoadableDetachableModel<String> + { + private static final long serialVersionUID = 1L; + + private int loads; + + @Override + protected String load() + { + loads++; + return "loaded"; + } + } + + private static class Bean implements Serializable + { + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + private final String name = "bean"; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/Component.java b/wicket-core/src/main/java/org/apache/wicket/Component.java index 9bef4432b0..15ac584aae 100644 --- a/wicket-core/src/main/java/org/apache/wicket/Component.java +++ b/wicket-core/src/main/java/org/apache/wicket/Component.java @@ -17,7 +17,10 @@ package org.apache.wicket; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -143,8 +146,9 @@ import org.slf4j.LoggerFactory; * Component becomes immutable. Attempts to alter the Component will result in a * WicketRuntimeException.</li> * <li><b>Detachment </b>- Each request cycle finishes by detaching all touched components. - * Subclasses should clean up their state by overriding {@link #onDetach()} or more specifically - * {@link #detachModels()} if they keep references to models beside the default model.</li> + * Subclasses should clean up their state by overriding {@link #onDetach()}. Models beside the + * default model are detached automatically when they are registered with + * {@link #addAdditionalModel(IModel)}.</li> * </ul> * </li> * <li><b>Visibility </b>- If a component is not visible (see {@link #setVisible(boolean)}) it will @@ -163,7 +167,12 @@ import org.slf4j.LoggerFactory; * The component's model can be passed in the constructor or set via * {@link Component#setDefaultModel(IModel)}. In neither case a model can be created on demand with * {@link #initModel()}.<br> - * Note that a component can have more models besides its default model.</li> + * A component can use further models beside its default model. Such a model is registered with + * {@link #addAdditionalModel(IModel)}, or passed to + * {@link #Component(String, IModel, IModel...)}, and is then detached together with the default + * model at the end of each request; {@link #getModels()} returns all of them. Registering costs + * memory, so a component rendered in large numbers is better off detaching its models itself, as + * the components shipped with Wicket do.</li> * <li><b>Behaviors </b>- You can add multiple {@link Behavior}s to any component if you need to * dynamically alter the behavior of components, e.g. manipulate attributes of the markup tag to * which a Component is attached. Behaviors take part in the component's lifecycle through various @@ -288,6 +297,12 @@ public abstract class Component private static final long serialVersionUID = 1L; }; + /** meta data for the models registered with {@link #addAdditionalModel(IModel)} */ + private static final MetaDataKey<IModel<?>[]> ADDITIONAL_MODELS_KEY = new MetaDataKey<>() + { + private static final long serialVersionUID = 1L; + }; + /** meta data for user specified markup id */ private static final MetaDataKey<FeedbackMessages> FEEDBACK_KEY = new MetaDataKey<>() { @@ -549,6 +564,35 @@ public abstract class Component } } + /** + * Constructor. All components have names. A component's id cannot be null. This constructor + * includes the default model and any number of additional models, which are registered as with + * {@link #addAdditionalModel(IModel)}. All of them are detached at the end of each request. + * + * @param id + * The non-null id of this component + * @param model + * The component's default model, may be null + * @param additionalModels + * The component's additional models, any of them may be null + * + * @throws WicketRuntimeException + * Thrown if the component has been given a null id. + * @since 11.0.0 + */ + public Component(final String id, final IModel<?> model, final IModel<?>... additionalModels) + { + this(id, model); + + if (additionalModels != null) + { + for (IModel<?> additionalModel : additionalModels) + { + addAdditionalModel(additionalModel); + } + } + } + /** * Let subclasses initialize this instance, before constructors are executed. <br> * This method is intentionally <b>not</b> declared protected, to limit overriding to classes in @@ -1046,12 +1090,23 @@ public abstract class Component } /** - * Detaches all models + * Detaches all models: the default model, see {@link #detachModel()}, and the models registered + * with {@link #addAdditionalModel(IModel)}. When a registered model is an {@link IWrapModel}, + * the model it wraps is detached as well. */ public void detachModels() { // Detach any detachable model from this component detachModel(); + + IModel<?>[] additionalModels = getMetaData(ADDITIONAL_MODELS_KEY); + if (additionalModels != null) + { + for (IModel<?> model : additionalModels) + { + detachModel(model, true); + } + } } /** @@ -2772,6 +2827,188 @@ public abstract class Component return this; } + /** + * Registers a model beside the default model, so that it is detached at the end of each request + * together with the default model. A component keeping further models in fields does not have + * to detach them itself: + * + * <pre> + * private final IModel<Foo> foo = addAdditionalModel(new FooModel()); + * </pre> + * + * The model is registered as given; unlike the default model it is not wrapped for this + * component. A component using an {@link IComponentAssignedModel} passes + * {@link #wrap(IModel) wrap(model)} instead. Registering a model that is already registered, or + * the model wrapped by a registered {@link IWrapModel}, has no effect. + * <p> + * Convenience at a price: a component with registered models costs roughly 50 to 80 bytes more + * than one detaching the same models by hand in {@link #onDetach()}, whatever the number of + * models, because they are kept as component meta data. That is worth it for a component used + * a few dozen times on a page and not for one rendered in the thousands, which is why the + * components shipped with Wicket keep detaching their models themselves. + * + * @param <M> + * the type of the model + * @param model + * the model to register, may be null + * @return the given model, so that it can be assigned in the same statement + * @see #replaceAdditionalModel(IModel, IModel) + * @see #removeAdditionalModel(IModel) + * @since 11.0.0 + */ + protected final <M extends IModel<?>> M addAdditionalModel(final M model) + { + if (model == null) + { + return null; + } + IModel<?>[] additionalModels = getMetaData(ADDITIONAL_MODELS_KEY); + if (indexOfAdditionalModel(additionalModels, model) >= 0) + { + return model; + } + + if (additionalModels == null) + { + additionalModels = new IModel<?>[] { model }; + } + else + { + additionalModels = Arrays.copyOf(additionalModels, additionalModels.length + 1); + additionalModels[additionalModels.length - 1] = model; + } + setMetaData(ADDITIONAL_MODELS_KEY, additionalModels); + return model; + } + + /** + * Replaces a model registered with {@link #addAdditionalModel(IModel)}, as a setter of a model + * field would: + * + * <pre> + * this.foo = replaceAdditionalModel(this.foo, foo); + * </pre> + * + * Unless both are the same model, the previous model is detached and unregistered, see + * {@link #removeAdditionalModel(IModel)}, and the given model is registered. + * + * @param <M> + * the type of the model + * @param previous + * the registered model to replace, may be null + * @param model + * the model to register, may be null to only unregister the previous model + * @return the given model, so that it can be assigned in the same statement + * @since 11.0.0 + */ + protected final <M extends IModel<?>> M replaceAdditionalModel(final IModel<?> previous, + final M model) + { + if (previous != model) + { + removeAdditionalModel(previous); + addAdditionalModel(model); + } + return model; + } + + /** + * Detaches and unregisters a model registered with {@link #addAdditionalModel(IModel)}. Given + * the model wrapped by a registered {@link IWrapModel}, the wrapper is unregistered. Removing a + * model that is not registered has no effect. + * + * @param <M> + * the type of the model + * @param model + * the model to unregister, may be null + * @return the given model + * @since 11.0.0 + */ + protected final <M extends IModel<?>> M removeAdditionalModel(final M model) + { + IModel<?>[] additionalModels = getMetaData(ADDITIONAL_MODELS_KEY); + int index = indexOfAdditionalModel(additionalModels, model); + if (index < 0) + { + return model; + } + detachModel(additionalModels[index], true); + + IModel<?>[] remainingModels = null; + if (additionalModels.length > 1) + { + remainingModels = new IModel<?>[additionalModels.length - 1]; + System.arraycopy(additionalModels, 0, remainingModels, 0, index); + System.arraycopy(additionalModels, index + 1, remainingModels, index, + remainingModels.length - index); + } + setMetaData(ADDITIONAL_MODELS_KEY, remainingModels); + return model; + } + + /** + * Gets all models of this component: the default model first, if there is one, followed by + * the models registered with {@link #addAdditionalModel(IModel)} in the order they were + * registered. + * Getting the models does not initialize a default model, see {@link #initModel()}. + * + * @return an unmodifiable collection of the models + * @since 11.0.0 + */ + public final Collection<IModel<?>> getModels() + { + IModel<?> defaultModel = getModelImpl(); + IModel<?>[] additionalModels = getMetaData(ADDITIONAL_MODELS_KEY); + if (additionalModels == null) + { + return defaultModel == null ? Collections.emptyList() + : Collections.singletonList(defaultModel); + } + List<IModel<?>> models = new ArrayList<>(additionalModels.length + 1); + if (defaultModel != null) + { + models.add(defaultModel); + } + Collections.addAll(models, additionalModels); + return Collections.unmodifiableList(models); + } + + /** + * Finds a registered model, either the given model itself or an {@link IWrapModel} wrapping it. + * + * @param additionalModels + * the registered models, may be null + * @param model + * the model to find, may be null + * @return the index of the registered model, or -1 if it is not registered + */ + private static int indexOfAdditionalModel(final IModel<?>[] additionalModels, + final IModel<?> model) + { + if (additionalModels != null && model != null) + { + for (int index = 0; index < additionalModels.length; index++) + { + IModel<?> additionalModel = additionalModels[index]; + if (additionalModel == model || unwrap(additionalModel) == model) + { + return index; + } + } + } + return -1; + } + + /** + * @param model + * a model + * @return the model wrapped by the given {@link IWrapModel}, or the given model otherwise + */ + private static IModel<?> unwrap(final IModel<?> model) + { + return model instanceof IWrapModel ? ((IWrapModel<?>)model).getWrappedModel() : model; + } + /** * @return model */ @@ -3376,16 +3613,33 @@ public abstract class Component */ protected void detachModel() { - IModel<?> model = getModelImpl(); + detachModel(getModelImpl(), !getFlag(FLAG_INHERITABLE_MODEL)); + } + + /** + * Detaches a model and, optionally, the model it wraps. + * + * @param model + * the model to detach, may be null + * @param detachWrappedModel + * whether the wrapped model of an {@link IWrapModel} is detached too; an inherited + * model is wrapped around the parent's model, which the parent detaches itself + */ + private static void detachModel(IModel<?> model, boolean detachWrappedModel) + { if (model != null) { model.detach(); } // also detach the wrapped model of a component assigned wrap (not // inherited) - if (model instanceof IWrapModel && !getFlag(FLAG_INHERITABLE_MODEL)) + if (model instanceof IWrapModel && detachWrappedModel) { - ((IWrapModel<?>)model).getWrappedModel().detach(); + IModel<?> wrappedModel = ((IWrapModel<?>)model).getWrappedModel(); + if (wrappedModel != null) + { + wrappedModel.detach(); + } } } diff --git a/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java b/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java index ad876b36e6..e7bd65b577 100644 --- a/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java +++ b/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java @@ -181,6 +181,16 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp super(id, model); } + /** + * @see Component#Component(String, IModel, IModel...) + * @since 11.0.0 + */ + public MarkupContainer(final String id, final IModel<?> model, + final IModel<?>... additionalModels) + { + super(id, model, additionalModels); + } + /** * Adds the child component(s) to this container. * diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/WebComponent.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/WebComponent.java index 3d7ac261c0..99cfe010c7 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/WebComponent.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/WebComponent.java @@ -54,6 +54,16 @@ public class WebComponent extends Component super(id, model); } + /** + * @see Component#Component(String, IModel, IModel...) + * @since 11.0.0 + */ + public WebComponent(final String id, final IModel<?> model, + final IModel<?>... additionalModels) + { + super(id, model, additionalModels); + } + @Override protected void onRender() { diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/WebMarkupContainer.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/WebMarkupContainer.java index 7be4716564..f70828f4bf 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/WebMarkupContainer.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/WebMarkupContainer.java @@ -53,6 +53,16 @@ public class WebMarkupContainer extends MarkupContainer super(id, model); } + /** + * @see Component#Component(String, IModel, IModel...) + * @since 11.0.0 + */ + public WebMarkupContainer(final String id, final IModel<?> model, + final IModel<?>... additionalModels) + { + super(id, model, additionalModels); + } + /** * A convenience method to return the WebPage. Same as getPage(). * diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/panel/GenericPanel.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/panel/GenericPanel.java index 9d7c4012e4..972fb0f4da 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/panel/GenericPanel.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/panel/GenericPanel.java @@ -48,4 +48,20 @@ public class GenericPanel<T> extends Panel implements IGenericComponent<T, Gener { super(id, model); } + + /** + * @param id + * the component id + * @param model + * the component model + * @param additionalModels + * the component's additional models + * @see org.apache.wicket.Component#Component(String, IModel, IModel...) + * @since 11.0.0 + */ + public GenericPanel(final String id, final IModel<T> model, + final IModel<?>... additionalModels) + { + super(id, model, additionalModels); + } } diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/panel/Panel.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/panel/Panel.java index ba3d9d9945..4a1e56648f 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/panel/Panel.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/panel/Panel.java @@ -75,6 +75,15 @@ public abstract class Panel extends WebMarkupContainer implements IQueueRegion super(id, model); } + /** + * @see org.apache.wicket.Component#Component(String, IModel, IModel...) + * @since 11.0.0 + */ + public Panel(final String id, final IModel<?> model, final IModel<?>... additionalModels) + { + super(id, model, additionalModels); + } + /** * {@inheritDoc} */ diff --git a/wicket-user-guide/src/main/asciidoc/modelsforms/modelsforms_8.adoc b/wicket-user-guide/src/main/asciidoc/modelsforms/modelsforms_8.adoc index fddc250796..b569958623 100644 --- a/wicket-user-guide/src/main/asciidoc/modelsforms/modelsforms_8.adoc +++ b/wicket-user-guide/src/main/asciidoc/modelsforms/modelsforms_8.adoc @@ -1,13 +1,11 @@ - - -Sometimes our custom components may need to use more than a single model to work properly. In such a case we must manually detach the additional models used by our components. In order to do this we can override the Component's onDetach method that is called at the end of the current request. The following is the generic code of a component that uses two models: +Sometimes our custom components may need to use more than a single model to work properly. Every model besides the default one must be detached at the end of the request as well. Since Wicket 11 a component can register such models with _addAdditionalModel_, and _Component_ then detaches them together with its default model. The method returns the model it was given, so it can be registered in the same statement that assigns it to a field. The following is the generic code of a component [...] [source,java] ---- /** * - * fooModel is used as main model while beeModel must be manually detached + * fooModel is used as main model while beeModel is registered as an additional model * */ public class ComponentTwoModels extends Component{ @@ -16,17 +14,44 @@ public class ComponentTwoModels extends Component{ public ComponentTwoModels(String id, IModel<Foo> fooModel, IModel<Bee> beeModel) { super(id, fooModel); - this.beeModel = beeModel; + this.beeModel = addAdditionalModel(beeModel); } - @Override - public void onDetach() { - if(beeModel != null) - beeModel.detach(); - - super.onDetach(); + public void setBeeModel(IModel<Bee> beeModel) { + this.beeModel = replaceAdditionalModel(this.beeModel, beeModel); } } ---- +_replaceAdditionalModel_ detaches and unregisters the previous model before registering the new one, and _removeAdditionalModel_ only unregisters a model. When the component needs no reference to its additional models, it can pass them to the constructor instead: + +[source,java] +---- +super(id, fooModel, beeModel); +---- + +_getModels()_ returns the default model followed by all additional models. + +Registering models is a convenience with a price: 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. 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. This is why the components shipped with Wicket detach their additional models themselves, as shown at the end of this section. + +Unlike the default model, an additional model is registered as it is given. If it implements _IComponentAssignedModel_ (like _ResourceModel_ or _CompoundPropertyModel_), the component wraps it first: + +[source,java] +---- +this.beeModel = addAdditionalModel(wrap(beeModel)); +---- + +With older versions of Wicket, for detachable state that is not a model, or when the memory of every single component instance counts, we must detach it manually. In order to do this we can override the Component's onDetach method that is called at the end of the current request: + +[source,java] +---- +@Override +public void onDetach() { + if(beeModel != null) + beeModel.detach(); + + super.onDetach(); +} +---- + When we override onDetach we must call the super class implementation of this method, usually as last line in our custom implementation.
