Models are the key
The most important part of being an effective Wicket developer is understanding how to use Models. The basic rule to remember is, always use Models! Using Models correctly is important for keeping your UI simple and avoid having out-dated data. It’s also important for performance reasons to keep the page size small. We’ll look at both of these, but first a general discussion of Models.
IModel interface
A Wicket Model is a class that implements the IModel interface. The interface is very simple, which consists of a getter method, setter method, and the type of the object. The setter can be excluded in the case of read-only models. Let’s look at an example model to retrieve/set a user’s email:
Model<String> userEmailModel = new Model<String>() {
@Override public String getObject() { return user.getEmail(); }
@Override public void setObject(String object) { user.setEmail(object); }
};
Now this is just for the purposes of demonstrating how simple a model implementation can be, but I would recommend you use other implementations of IModel that are provided by Wicket. Fortunately, you can use LambaModel’s which we looked at in a previous post!
Let’s look at an example of how to use this model in a edit user form page:
TextField<String> emailField = new TextField<String>("email", userEmailModel);
Using Models to keep UI simple and synced
Let’s look at a slightly more complex example of a user profile page which contains the user’s country and state selections. Selecting the country will update the state dropdown. We also have a label that displays the user’s address in the format {state, Country}. Using models, we can implement it in this way:
countryField = new DropDownChoice<CountryCode>("country", LambdaModel.of(user::getCountryCode, user::setCountryCode), new ListModel<CountryCode>(CountryCode.VALUES));
countryField.add(new OnChangeAjaxBehavior() {
@Override protected void onUpdate(AjaxRequestTarget target) { target.add(stateField, stateCountryLbl); }
});
testForm.addOrReplace(countryField);
stateField = new DropDownChoice<State>("state", LambdaModel.of(user::getState, user::setState), new LoadableDetachableModel<List<State>>() {
@Override
protected List<State> load() {
return user.getCountryCode() == null ? Collections.emptyList() : State.getByCountry(user.getCountryCode());
}
});
stateField.add(new OnChangeAjaxBehavior() {
@Override protected void onUpdate(AjaxRequestTarget target) { target.add(stateCountryLbl); }
});
testForm.addOrReplace(stateField.setOutputMarkupId(true));
stateCountryLbl = new Label("stateCountry", new LoadableDetachableModel<String>() {
@Override
protected String load() {
return String.format("%s, %s", user.getState() != null ? user.getState().getAbbreviation() : "",
user.getCountryCode() != null ? user.getCountryCode().getName() : "");
}
});
testForm.addOrReplace(stateCountryLbl.setOutputMarkupId(true));
So we first have a country selection which updates the state dropdown options upon being changed. We can see that when the countryField is updated, it adds the stateField to the target which re-renders it. The stateField uses a LDM(LoadableDetachableModel) to return the State’s based on the country selection.
The most important thing you should notice is that because we are using models correctly, we never have to manually update the options for the stateField; they are always up to date. Nor do we have to manually update the value of stateCountryLbl.
A wrong way to do it
We’ve seen a correct way to do it, now let’s show a typical implementation you might see without a good understanding of models:
countryField = new DropDownChoice<CountryCode>("country", LambdaModel.of(user::getCountryCode, user::setCountryCode), new ListModel<CountryCode>(CountryCode.VALUES));
countryField.add(new OnChangeAjaxBehavior() {
@Override protected void onUpdate(AjaxRequestTarget target) {
stateField.setChoices(user.getCountryCode() == null ? Collections.emptyList() : State.getByCountry(user.getCountryCode()));
stateCountryLbl.setDefaultModelObject(getSateCountryLbl());
target.add(stateField, stateCountryLbl);
}
});
testForm.addOrReplace(countryField);
stateField = new DropDownChoice<State>("state", LambdaModel.of(user::getState, user::setState), user.getCountryCode() == null ? Collections.emptyList() : State.getByCountry(user.getCountryCode()));
stateField.add(new OnChangeAjaxBehavior() {
@Override protected void onUpdate(AjaxRequestTarget target) {
stateCountryLbl.setDefaultModelObject(getSateCountryLbl());
target.add(stateCountryLbl);
}
});
testForm.addOrReplace(stateField.setOutputMarkupId(true));
stateCountryLbl = new Label("stateCountry", getSateCountryLbl());
testForm.addOrReplace(stateCountryLbl.setOutputMarkupId(true));
private String getSateCountryLbl() {
return String.format("%s, %s", user.getState() != null ? user.getState().getAbbreviation() : "",
user.getCountryCode() != null ? user.getCountryCode().getName() : "");
}
Notice the highlighted lines. Without models, we are now responsible for manually keeping the value of stateCountryLbl updated as well as the options for stateField. It might not seem like a huge difference from this example, but in more complex real-world UI’s, this type of approach will create a lot of problems.
Performance impact on Serialization
The other issue is the impact on Wicket page size. When you don’t correctly use detachable models such as LoadableDetachableModel, you might inadvertently be serializing large lists of objects. Let’s look at an example of a dropdown that displays a user selection:
List<User> allUsers = userService.getAllUsers();
DropDownChoice<User> userSelectionField = new DropDownChoice<User>("userSelection", new PropertyModel<User>(this, "selectedUser"), allUsers);
It might not seem obvious, but when the page is serialized by Wicket, the allUser list will be serialized along with the page. Imagine you have 5K users in your database! The correct way of doing this, is to use a detachable model:
DropDownChoice<User> userSelectionField = new DropDownChoice<User>("userSelection", new PropertyModel<User>(this, "selectedUser"), new LoadableDetachableModel<List<User>>() {
@Override protected List<User> load() { return userService.getAllUsers(); }
});
Now the list of users does not get serialized along with the page. You need to always be aware of large objects or lists of objects being serialized.
Hopefully this has given you a good handle on using models correctly, I will probably write more posts about the topic in the future. Go forth and be a model citizen(developer) 馃檪