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&lt;Foo&gt; 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.

Reply via email to