...
- Wicket provides
IModel
implementations you can use with any component. These models can do things such as retrieve the value from a resource file, or read and write the value from a Java Bean property. - Wicket also provides
IModel
implementations that defer retrieving the value until it is actually needed, and remove it from the servlet Session when the request is complete. This reduces session memory consumption , and it is particularly useful with large values such as lists. - Unlike Swing, you do not have to implement an extra interface or helper class for each different component. Especially for the most often used components such as Labels and TextFields you can easily bind to a bean property.
- In many cases you can provide the required value directly to the component and it will wrap a default model implementation around it for you.
- And while you so do not have to use beans as your models as you must with Struts, you may still easily use beans if you wish. Wicket provides the appropriate model implementations.
...
Code Block |
---|
personForm.add(new RequiredTextField("personName", new Model() {
@Override
public Object getObject(Component component) {
return person.getName();
}
@Override
public void setObject(Serializable object) {
person.setName((String) object);
}
}));
|
...
The PropertyModel class allows you to create a model that accesses a particular property of its associated model object at runtime. This property is accessed using a simple expression language with a dot notation (e.g. 'name'
means property 'name'
, and 'person.name'
means property name
of property object person
). The simplest PropertyModel constructor is:
...
which takes a model object and a property expression. When the property model is asked for its value by the framework, it will use the property expression to access the model object's property. For example, if we have a Java Bean or "POJO" (Plain Old Java Object) like this:
Code Block |
---|
class Person { private String name; Person(String name) { this.name = name; } String getName() { return name; } } |
then the property expression "name
" can be used to access the "name
" property of any Person object via the getName()
getter method.
...
The corresponding input field in the html must have a wicket id of "address.city". This works, but it does expose the internal structure of the model data in the html. CompoundPropertyModel
has a subclass, BoundCompoundPropertyModel
, method that can be used to rectify this.
BoundCompoundPropertyModel
adds three new methods. They associate a child component of the model with a specific property expression and/or type conversionThe model associates a different property expression with the component being bound.
Code Block |
---|
public <S> ComponentIModel<S> bind(final Component component, final String propertyExpression) public Component bind(final Component component, final Class type) public Component bind(final Component component, final String propertyExpression, final Class type) property) |
With With this association in place the child component can have whatever name we like, rather than having the match the property expression.
To use BoundCompoundPropertyModel
CompoundPropertyModel.bind
for the city field discussed above we might do something like this:
Code Block |
---|
BoundCompoundPropertyModelCompoundPropertyModel personModel = new BoundCompoundPropertyModelCompoundPropertyModel(person); Form personForm = new Form("aPersonForm", personModel); TextField cityField = new RequiredTextField("city"); personForm.add(cityField); , personModel.bind(cityField, "address.city")); |
Note that the bind methods return the child component, thus the above can be more compactly written:
Code Block |
---|
BoundCompoundPropertyModel personModel = new BoundCompoundPropertyModel(person);
Form personForm = new Form("aPersonForm", personModel);
personForm.add(personModel.bind(new RequiredTextField("city"), "address.city"));
|
personForm.add(cityField);
|
Also, note that if you are using a component that you Also, note that if you are using a component that you do not want to reference the compound property model, but is a child of the form, that you define a model for that component. For example:
...
These are the Address and Person classes used in the previous examples:
Code Block |
---|
public class Address implements Serializable { private String city; public Address() { super super(); } public String getCity() { return city; } public void setCity(String city) { this.city = city; } } public class Person implements Serializable { private String name; private int age; private Address address; public Person() { super(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } } |
The first step is to create a Wrapped Object Model for the Address and Person classes:
Code Block |
---|
public class AddressModel implements IModel { private IModel addressContainingModel; private AddressModelType type; public enum AddressModelType { CITY_MODEL; }; public AddressModel(IModel addressContainingModel, AddressModelType type) { this.addressContainingModel = addressContainingModel; this.type = type; } @Override public Object getObject() { Address address = (Address) addressContainingModel.getObject(); switch (type) { case CITY_MODEL: return address.getCity(); } throw new UnsupportedOperationException ("invalid AddressModelType = " + type.name()); } @Override public void setObject(Object object) { Address address = (Address) addressContainingModel.getObject(); switch (type) { case CITY_MODEL: address.setCity((String) object); break; default: throw new UnsupportedOperationException("invalid AddressModelType = break;" default: throw new UnsupportedOperationException ("invalid AddressModelType = " + type.name()); } } @Override public void detach() { addressContainingModel.detach(); } } public class PersonModel implements IModel { private IModel personContainingModel; private PersonModelType type; public enum PersonModelType { NAME_MODEL, AGE_MODEL, ADDRESS_MODEL; } public PersonModel(IModel personContainingModel, PersonModelType type) { this.personContainingModel = personContainingModel; this.type = type; } @Override public Object getObject() { Person person = (Person) personContainingModel.getObject(); switch (type) { case NAME_MODEL: return person.getName(); case AGE_MODEL: return new Integer (person.getAge()); case ADDRESS_MODEL: return person.getAddress(); } throw new UnsupportedOperationException ("invalid PersonModelType = " + type.name()); } @Override public void setObject(Object object) { Person person = (Person) personContainingModel.getObject(); switch (type) { case NAME_MODEL: person.setName((String) object); break; break; case AGE_MODEL: person.setAge((Integer) object); break; case ADDRESS_MODEL: person.setAddress((Address) object); break; case ADDRESS_MODEL: person.setAddress ((Address) object); default: throw new UnsupportedOperationException("invalid PersonModelType = " break; default: throw new UnsupportedOperationException ("invalid PersonModelType = " + type.name()); } } } @Override public void detach() { personContainingModel.detach(); } } |
Notice how each wrapped model contains an inner model that contains the actual pojo instance. This allows for the wrapped model to be a plain Model or a LoadableDetachableModel, or even another wrapped model where its .getObject() results in a suitably typed input value (see the "address.city" field in the example below).
At this point to create a form using our wrapped object models looks like:
Code Block |
---|
Person p = new Person (); Form personForm = new Form("aPersonForm"); personForm.add(new RequiredTextField("name", new PersonModel (new Model (p), PersonModelType.NAME_MODEL))); personForm.add(new RequiredTextField("age", new PersonModel (new Model (p), PersonModelType.AGE_MODEL), Integer.class)); personForm.add(new RequiredTextField("address.city", new AddressModel (new PersonModel (new Model (p), PersonModelType.ADDRESS_MODEL), AddressModelType.CITY_MODEL))); add(personForm); |
A wrapped object model also makes working with DataTables's easier as one IColumn implementation can be written for each object class which makes the declaration of the table much simpler.
...
Code Block |
---|
public class PersonTableColumn extends AbstractColumn { private final PersonModelType type; /** * */ public PersonTableColumn(String columnName, PersonModelType type) { super super(new Model (columnName)); this.type = type; } @Override public void populateItem(Item cellItem, String componentId, IModel rowModel) { switch (type) { switch (type) { // this needs to be handled seperately due to it being an Address // instance from the Person object. case ADDRESS_MODEL: cellItem cellItem .add(new Label (componentId, new AddressModel (new PersonModel (rowModel, PersonModelType.ADDRESS_MODEL), AddressModelType.CITY_MODEL))); new AddressModel(new PersonModel(rowModel, break; default: cellItem.add(new Label (componentId, new PersonModel (rowModel, type))); } } } |
So the table could be declared like:
Code Block |
---|
DataTable table = new DataTable("datatable", new IColumn[] {new PersonTableColumn ("Name", PersonModelType.NAME_MODEL), new PersonTableColumn ("Age", PersonModelType.AGE_MODEL), new PersonTableColumn ("City", PersonModelType.ADDRESS_MODEL)}, new PersonProvider(), 10); |
Another option with the complex object is to create a custom IConverter that will take in this case the Address instance from the PersonModel and render the string value as the city name.
e.g.
Code Block |
---|
public class CustomLabel extends Label {
private final IConverter converter;
/**
* @param id
* @param label
*/
public CustomLabel(String id, IModel labelModel, IConverter converter) {
super(id, labelModel);
this.converter = converter;
}
/* (non-Javadoc)
* @see org.apache.wicket.Component#getConverter(java.lang.Class)
*/
@Override
public IConverter getConverter(Class type) {
return this.converter;
}
}
|
With the populate from above as:
Code Block |
---|
public void populateItem(Item cellItem, String componentId, IModel rowModel) {
switch (type) {
case ADDRESS_MODEL:
cellItem.add(new CustomLabel (componentId, new PersonModel (rowModel, PersonModelType.ADDRESS_MODEL), new IConverter () {
/* (non-Javadoc)
* @see org.apache.wicket.util.convert.IConverter#convertToObject(java.lang.String, java.util.Locale)
*/
@Override
public Object convertToObject(String arg0, Locale arg1) {
throw new UnsupportedOperationException ("converter is readonly.");
}
/* (non-Javadoc)
* @see org.apache.wicket.util.convert.IConverter#convertToString(java.lang.Object, java.util.Locale)
*/
@Override
public String convertToString(Object object, Locale locale) {
Address address = (Address) object;
return address.getCity();
}}));
break;
default:
cellItem.add(new Label (componentId, new PersonModel (rowModel, type)));
}
}
|
Resource Models
Localization of resources in Wicket is supported by the Localizer
class. This provides a very convenient way to retrieve and format application-wide or component-specific resources according to the Locale that the user's Session is running under. You can retrieve a String
using Localizer
and use the result as a model object, but it is usually more convenient to use one of Wicket's resource model classes.
ResourceModel
is the simplest of these. It simply takes a resource key and uses it to determine the model object (by looking for the resource in a property file). Thus for a example a label can be created this way:
Code Block |
---|
add(new Label("greetings", new ResourceModel("label.greetings")));
|
(Note however that you can also accomplish the same thing using the wicket:message tag in the html.)
ResourceModel
has another constructor which takes an additional string to be used as a default if the resource is not found.
The model objects created via ResourceModel
vary according to the locale and the contents of the resource bundle, but they don't otherwise depend on the program state. For example, the greeting message produced above doesn't contain the user's name. More dynamic models can be constructed using StringResourceModel
.
StringResourceModels have a resource key, a Component, an optional model and an optional array of parameters that can be passed to the Java MessageFormat facility. The constructors look like this:
Code Block |
---|
public StringResourceModel(final String resourceKey, final Component component, final IModel model)
public StringResourceModel(final String resourceKey, final Component component, final IModel model, final Object[] parameters)
|
The resourceKey is used to find the resource in a property file. The property file used depends on the component.
A very simple example of a StringResourceModel
might be a Label constructed like this:
Code Block |
---|
add(new Label("greetings", new StringResourceModel("label.greetings", this, null)));
|
where the resource bundle for the page contains an entry like this:
Code Block |
---|
label.greetings=Welcome!
|
This label is essentially the same as that constructed with ResourceModel
above. However, with StringResourceModel
we can add the name from a User object to the greeting. We pass the user object as the model parameter to the constructor. Wicket will find the localized string in the resource bundle, find any substrings within it of the form ${propertyexpression}, and replace these substrings with their values as property expressions. The model is used to evaluate the property expressions.
For example, suppose we wanted to add the name from a User object to the greeting above. We'd say:
Code Block |
---|
User user;
...
add(new Label("greetings", new StringResourceModel("label.greetings", this, new Model(user))));
|
and have a resource like:
Code Block |
---|
label.greetings=Welcome, ${name}!
|
where User has a getName()
method exposing its "name"
property.
Note that StringResourceModel
is dynamic: the property expression is re-evaluated every time getObject()
is called. In contrast, if the application code called Localizer
itself and used the resulting string as model data, the result would be a static (non-changing) model.
Detachable Models
Wicket maintains information about recently created pages in the servlet Session. The models for the pages' components are a large portion of this data. There are several motivations for reducing the amount of memory consumed by this data. It increases the number of users that can be served with the same amount of memory. Also, we may wish to replicate session data in a cluster so we can move sessions if a web server fails, and in this case there is serialization overhead we want to minimize.
It's quite common that model data contains some very small sub-part, such as a database identifier, from which the rest of the object can be reconstituted. So one way to reduce Session memory usage is to discard the data when display of the page is complete, except for the small special piece needed to recover the rest. Wicket supports this via detachable models.
The easiest way to create a detachable model is to extend LoadableDetachableModel
. This class has an abstract method
Code Block |
---|
protected abstract java.lang.Object load();
|
For example:
Code Block |
---|
class LoadablePersonModel extends LoadableDetachableModel {
Long id;
LoadablePersonModel(Long id) {
this.id = id;
}
@Override
protected Object load() {
return DataManager.getPersonFromDatabase(id);
}
}
|
When getObject()
is called for the first time on a given instance of this class, it will call load()
to retrieve the data. At this point the data is said to be attached. Subsequent calls to getObject()
will return the attached data. At some point (after the page has been rendered) this data will be removed (the internal references to it will be set to null). The data is now detached. If getObject()
is subsequently called (for example, if the page is rendered again), the data will be attached again, and the cycle starts anew.
Chaining models
Suppose you want a model that is both a loadable model and a compound model, or some other combination of the model types described above. One way to achieve this is to chain models, like this:
Code Block |
---|
CompoundPropertyModel personModel = new CompoundPropertyModel(new LoadablePersonModel(personId));
|
(here LoadablePersonModel
is a subclass of LoadableDetachableModel
, as described earlier)
The model classes in the core Wicket distribution support chaining where it make sense.
More about the IModel interface
We are now in a position to understand the complete IModel
interface:
Code Block |
---|
public interface IModel extends IDetachable
{
public Object getNestedModel();
public Object getObject(final Component component);
public void setObject(final Component component, final Object object);
}
|
IModel
extends IDetachable
, which means that all models must provide a method public void detach()
. Some model classes (Model
for one) provide an empty, no-op implementation.
In some cases model instances form a natural hierarchy. For example, several CompoundPropertyModels
share the same model object. The getNestedModel()
method will return this common shared object. In cases where there is no such object it returns null
.
Compound models are also the motivation for the component
parameters to getObject
and setObject
. Several components may share the same CompoundPropertyModel
object. If getObject
(or setObject
) is called it must return something different depending on the component (e.g., evaluate the appropriate property expression). Thus these two methods must be passed a component as a parameter.
,
AddressModelType.CITY_MODEL)));
break;
default:
cellItem.add(new Label(componentId, new PersonModel(rowModel, type)));
}
}
}
|
So the table could be declared like:
Code Block |
---|
DataTable table = new DataTable("datatable", new IColumn[] {
new PersonTableColumn("Name", PersonModelType.NAME_MODEL),
new PersonTableColumn("Age", PersonModelType.AGE_MODEL),
new PersonTableColumn("City", PersonModelType.ADDRESS_MODEL)
}, new PersonProvider(), 10);
|
Another option with the complex object is to create a custom IConverter that will take in this case the Address instance from the PersonModel and render the string value as the city name.
e.g.
Code Block |
---|
public class CustomLabel extends Label {
private final IConverter converter;
/**
* @param id
* @param label
*/
public CustomLabel(String id, IModel labelModel, IConverter converter) {
super(id, labelModel);
this.converter = converter;
}
/* (non-Javadoc)
* @see org.apache.wicket.Component#getConverter(java.lang.Class)
*/
@Override
public IConverter getConverter(Class type) {
return this.converter;
}
}
|
With the populate from above as:
Code Block |
---|
public void populateItem(Item cellItem, String componentId, IModel rowModel)
{
switch (type)
{
case ADDRESS_MODEL:
cellItem
.add(new CustomLabel(componentId,
new PersonModel(rowModel,
PersonModelType.ADDRESS_MODEL),
new IConverter()
{
public Object convertToObject(String arg0,
Locale arg1)
{
throw new UnsupportedOperationException("converter is readonly.");
}
public String convertToString(Object object,
Locale locale)
{
Address address = (Address) object;
return address.getCity();
}
}));
break;
default:
cellItem.add(new Label(componentId, new PersonModel(rowModel, type)));
}
}
|
Resource Models
ResourceModel
Localization of resources in Wicket is supported by the Localizer
class. This provides a very convenient way to retrieve and format application-wide or component-specific resources according to the Locale that the user's Session is running under. You can retrieve a String
using Localizer
and use the result as a model object, but it is usually more convenient to use one of Wicket's resource model classes.
ResourceModel
is the simplest of these. It simply takes a resource key and uses it to determine the model object (by looking for the resource in a property file). Thus for a example a label can be created this way:
Code Block |
---|
add(new Label("greetings", new ResourceModel("label.greetings")));
|
(Note however that you can also accomplish the same thing using the wicket:message tag in the html.)
ResourceModel
has another constructor which takes an additional string to be used as a default if the resource is not found.
The model objects created via ResourceModel
vary according to the locale and the contents of the resource bundle, but they don't otherwise depend on the program state. For example, the greeting message produced above doesn't contain the user's name. More dynamic models can be constructed using StringResourceModel
.
Note that the ResourceModel
, like other models that use the ComponentStringResourceLoader, takes two attempts to load each key it's looking for. I.e., in addition to the various style/locale/component-path retries, it looks using both a relative path and an absolute path. To try & clarify using the example above, it will look for both the property "greetings.label.greetings" (assuming the Label is directly added to the page) as well as the absolute "label.greetings" key. If the label was part of a form, it could be "formName.greetings.label.greetings" (etc...).
StringResourceModel
StringResourceModels have a resource key, a Component, an optional model and an optional array of parameters that can be passed to the Java MessageFormat facility. The constructors look like this:
Code Block |
---|
public StringResourceModel(final String resourceKey, final Component component, final IModel model)
public StringResourceModel(final String resourceKey, final Component component, final IModel model, final Object[] parameters)
|
The resourceKey is used to find the resource in a property file. The property file used depends on the component.
A very simple example of a StringResourceModel
might be a Label constructed like this:
Code Block |
---|
add(new Label("greetings", new StringResourceModel("label.greetings", this, null)));
|
where the resource bundle for the page contains an entry like this:
Code Block |
---|
label.greetings=Welcome!
|
This label is essentially the same as that constructed with ResourceModel
above. However, with StringResourceModel
we can add the name from a User object to the greeting. We pass the user object as the model parameter to the constructor. Wicket will find the localized string in the resource bundle, find any substrings within it of the form ${propertyexpression}, and replace these substrings with their values as property expressions. The model is used to evaluate the property expressions.
For example, suppose we wanted to add the name from a User object to the greeting above. We'd say:
Code Block |
---|
User user;
...
add(new Label("greetings", new StringResourceModel("label.greetings", this, new Model(user))));
|
and have a resource like:
Code Block |
---|
label.greetings=Welcome, ${name}!
|
where User has a getName()
method exposing its "name"
property.
Note that StringResourceModel
is dynamic: the property expression is re-evaluated every time getObject()
is called. In contrast, if the application code called Localizer
itself and used the resulting string as model data, the result would be a static (non-changing) model.
Detachable Models
At the end of each request cycle the active page is serialized into a byte array and stored for the next request (some in the session and some to disk: see IPageStore
for more info). The entire object graph of the page including all components and their associated models are included in generated byte stream (byte array). Once you get past all but the most trivial of applications this object graph becomes huge and the time required to serialize it becomes a bottle neck. Keeping the size of the serialized pages low will increase the number of concurrent users your application can support. It will also minimize the amount of time required to fail over to another cluster node and the bandwidth to keep the sessions replicated.
The Wicket solution to this problem is to create the concept of detachability where by at the end of the request cycle all the components contained in a page are recursively detached (See Component.detatch()) which dramatically reduces the size of the object graph being serialized. Each component in turn then detaches its internal state including the value of its default model by calling the IModel.detach()
method.
So a Detachable Model is an IModel that releases most of its object graph when you call detatch()
retaining just enough detail (typically a unique identifier) that can be used to reconstitute the object on the next call to getObject()
which will occur on the next page render.
Two commonly used built-in Wicket IModel implementations are:
- AbstractReadOnlyModel
- LoadableDetachableModel (also commonly referred to as an LDM)
It is also possible to create a custom IModel
implementation that mirrors the LoadableDetchableModel
inner workings but also supporting IModel.setObject()
.
Managing the lifecycle of non default detachable IModel's
Wicket provides the detachability hooks and will automatically detach the default model of each component. However its common to use more than a single IModel
implementation per component especially when using nested or chaining models. In this case the developer is responsible for registering their custom models to participate in the detachment process.
The best way to handle this registration is to make sure it occurs in the same scope as where the IModel
implementation was instantiated.
For example:
Code Block |
---|
public class MyPanel extends Panel {
private final DetatchableModelA detachableModelA;
public MyPanel (String id) {
super(id);
this.detachableModelA = new DetachableModelA();
add (new CustomFormPanel ("formPanel", new CListModelFromA (this.detachableModelA)));
}
@Override
public void detach() {
// remember to call the parent detach logic
super.detach();
this.detachableModelA.detach();
}
}
|
Here you see that the DetachableModelA
instance is created in the MyPanel
constructor and then passed in as the inner model to the CListModelFromA
model which will load a List<C>
instances based on the details of the A
instance contained in the model. Because the this.detachableModelA
instance could possibly be shared like this in many sub component models its important that the CListModelFromA
implementation does not try and call detach()
on it but rather leave the detachment to occur in the same scope that the model was instantiated. In this case the detachment is done in the MyPanel.detach
.
AbstractReadOnlyModel
When you subclass AbstractReadOnlyModel
you implement the:
Code Block |
---|
public abstract T getObject();
|
So long as your subclass does not contain any private fields then no business data will be serialized when the the request cycle ends. The model object value is computed
at the time the method is called and not cached anywhere.
For example:
Code Block |
---|
public class PersonNameModel extends AbstractReadOnlyModel<String> {
private final IModel<Person>personModel;
public PersonNameModel (IModel<Person>personContainingModel> personModel) {
this.personModel = personModel;
}
public String getObject() {
Person p = this.personModel.getObject();
if (p == null)
return "";
return p.getName();
}
}
|
You can see it takes in an existing IModel
that contains the Person object and that the getObject()
method extracts the name of the person.
LoadableDetachableModel
This class extends AbstractReadOnlyModel
and finalizes the IModel.getObject()
method requiring you to implement:
Code Block |
---|
protected abstract T load();
|
This method is called once per request cycle, the first time that the getObject()
method is invoked. The T object value is then cached in a transient private field and used as the return value to all subsequent calls to getObject()
.
For example:
Code Block |
---|
class LoadablePersonModel extends LoadableDetachableModel<Person> {
Long id;
LoadablePersonModel(Long id) {
this.id = id;
}
@Override
protected Person load() {
return DataManager.getPersonFromDatabase(id);
}
}
|
When getObject()
is called for the first time on a given instance of this class, it will call load()
to retrieve the data. At this point the data is said to be attached. Subsequent calls to getObject()
will return the attached data. At some point (after the page has been rendered) this data will be removed (the internal references to it will be set to null). The data is now detached. If getObject()
is subsequently called (for example, if the page is rendered again), the data will be attached again, and the cycle starts anew.
Read/Write Detachable Model
Here is an example of how to create a model that is loadable and detachable but also supports using the IModel.setObject(T value)
to store a different value into the model.
Code Block |
---|
public class LoadableDetachableAttributeModel implements
IModel<Attribute> {
private transient Attribute cachedAttribute = null;
private transient boolean attached = false;
private String attributeName = null;
private final AttributeService service;
public LoadableDetachableAttributeModel (AttributeService service) {
this.service = service;
}
public LoadableDetachableAttributeModel (AttributeService service, Attribute defaultValue) {
this (service);
if (defaultValue != null) {
setObject(defaultValue);
}
}
/* (non-Javadoc)
* @see org.apache.wicket.model.IDetachable#detach()
*/
@Override
public void detach() {
attached = false;
cachedAttribute = null;
}
/* (non-Javadoc)
* @see org.apache.wicket.model.IModel#getObject()
*/
@Override
public Attribute getObject() {
if (!attached) {
// load the attribute
attached = true;
if (attributeName != null) {
// load the attribute from the service
this.cachedAttribute = service.loadAttribute (this.attributeName);
}
}
return this.cachedAttribute;
}
/* (non-Javadoc)
* @see org.apache.wicket.model.IModel#setObject(java.lang.Object)
*/
@Override
public void setObject(Attribute object) {
this.attached = true;
if (object == null) {
this.attributeName = null;
this.cachedAttribute = null;
}
else {
attributeName = object.getName();
this.cachedAttribute = object;
}
}
|
Chaining models
Suppose you want a model that is both a loadable model and a compound model, or some other combination of the model types described above. One way to achieve this is to chain models, like this:
Code Block |
---|
CompoundPropertyModel personModel = new CompoundPropertyModel(new LoadablePersonModel(personId));
|
(here LoadablePersonModel
is a subclass of CompoundPropertyModel
, as described earlier)
The model classes in the core Wicket distribution support chaining where it make sense.
More about the IModel interface
We are now in a position to understand the complete IModel
interface:
Code Block |
---|
public interface IModel extends IDetachable
{
public Object getNestedModel();
public Object getObject(final Component component);
public void setObject(final Component component, final Object object);
}
|
IModel
extends IDetachable
, which means that all models must provide a method public void detach()
. Some model classes (Model
for one) provide an empty, no-op implementation.
In some cases model instances form a natural hierarchy. For example, several CompoundPropertyModels
share the same model object. The getNestedModel()
method will return this common shared object. In cases where there is no such object it returns null
.
Compound models are also the motivation for the component
parameters to getObject
and setObject
. Several components may share the same CompoundPropertyModel
object. If getObject
(or setObject
) is called it must return something different depending on the component (e.g., evaluate the appropriate property expression). Thus these two methods must be passed a component as a parameter.
Panel | ||
---|---|---|
The IModel interface was simplified in Wicket 2.0:
The get and set methods do not take a component argument anymore. Instead, Wicket 2.0 has specialized model interfaces to do with specific issues like recording the 'owning' component of a model. See IAssignmentAwareModel and IInheritableModel (though you typically don't need to know these interfaces directly). Another change is that IModel does now support generics. This is especially interesting when authoring custom components where you allow only models (compile time) that produce a certain type. ListView for instance only accepts models that produces instances of java.util.List. |
Refactor Safe Property Models
Annotation processors
There are a number of annotation processors that generate a meta data that can be used to build safe property models. Examples of such processors:
- https://github.com/42Lines/metagen MetaGen
- http://bindgen.org/ Bindgen together with http://code.google.com/p/bindgen-wicket/ Bindgen-Wicket module
- Hibernate metamodel processor's metamodel can be used for this purpose with a custom model implementation
LambdaJ
With a little bit of help from the LambdaJ project we can stop using fragile PropertyModels.
Code Block |
---|
/* www.starjar.com
* Copyright (c) 2011 Peter Henderson. All Rights Reserved.
* Licensed under the Apache License, Version 2.0
*/
package com.starjar.wicket;
import ch.lambdaj.function.argument.Argument;
import ch.lambdaj.function.argument.ArgumentsFactory;
import org.apache.wicket.model.PropertyModel;
public class ModelFactory {
/**
* Use with the on() function from Lambdaj to have refactor safe property models.
*
* e.g.
* <pre>
* import static com.starjar.wicket.ModelFactory.*;
* import static ch.lambdaj.Lambda.*;
*
* Contact contact = getContactFromDB();
*
* Label l = new Label("id", model(contact, on(Contact.class).getFirstName()));
*
*
* </pre>
*
* OR
*
* <pre>
* import static com.starjar.wicket.ModelFactory.*;
* import static ch.lambdaj.Lambda.*;
*
* ContactLDM contactLDM = new ContactLDM(contactId);
*
* Label l = new Label("id", model(contactLDM, on(Contact.class).getFirstName()));
* </pre>
*
* Works as expected for nested objects
*
* <pre>
* Label l = new Label("address", model(contactLDM, on(Contact.class).getAddress().getLine1()));
* </pre>
*
*
* @param <T> Type of the model value
* @param value
* @param proxiedValue
* @return
*/
public static <T> PropertyModel<T> model(Object value, T proxiedValue) {
Argument<T> a = ArgumentsFactory.actualArgument(proxiedValue);
String invokedPN = a.getInkvokedPropertyName();
PropertyModel<T> m = new PropertyModel<T>(value, invokedPN);
return m;
}
}
|
Which can then be used
Code Block | ||
---|---|---|
import static ch.lambdaj.Lambda.*;
import static com.starjar.wicket.ModelFactory.*;
public class MyPanel extends Panel {
public MyPanel(String id) {
Label l = new Label("firstName", model(contact, on(Contact.class).getFirstName());
add(l);
Label addr = new Label("address", model(contact, on(Contact.class).getAddress().getLine1());
add(addr);
}
}
| ||
Panel | ||
The IModel interface was simplified in Wicket 2.0:
The get and set methods do not take a component argument anymore. Instead, Wicket 2.0 has specialized model interfaces to do with specific issues like recording the 'owning' component of a model. See IAssignmentAwareModel and IInheritableModel (though you typically don't need to know these interfaces directly). Another change is that IModel does now support generics. This is especially interesting when authoring custom components where you allow only models (compile time) that produce a certain type. ListView for instance only accepts models that produces instances of java.util.List. |