A common question on Wicket mailing lists is “Why doesn’t my ListView work as expected in a Form?”. There can be many possible answers, but the core issue is that ListView was never designed to work as a form component. ListView is great when it comes to displaying a list of elements, but in order to be a good citizen in Wicket’s form processing a list editor should possess the following features:
- Preserve component hierarchy that represents list items – this means that components the user added to represent an item in the list should be preserved even if list items are shuffled around. This will allow things like feedback messages and error indicators to work seamlessly and transparently
- Implement atomic form updates – this is perhaps the most important feature. If the user moves an item up or down in the list this change should not be reflected in the model object until the form is submitted. Same goes for actions such as item additions and removals.
- Should be reusable for different usecases
Surprisingly, implementing such a component is trivial. We will start by implementing the ListEditor itself and its one supporting class: ListItem, and later move on to implementing a Remove button. As always the fully functional sample project will be attached at the bottom.
Lets begin with the source to the ListEditor class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | public abstract class ListEditor<T> extends RepeatingView implements IFormModelUpdateListener { List<T> items; public ListEditor(String id, IModel<List<T>> model) { super(id, model); } protected abstract void onPopulateItem(ListItem<T> item); public void addItem(T value) { items.add(value); ListItem<T> item = new ListItem<T>(newChildId(), items.size() - 1); add(item); onPopulateItem(item); } protected void onBeforeRender() { if (!hasBeenRendered()) { items = new ArrayList<T>(getModelObject()); for (int i = 0; i < items.size(); i++) { ListItem<T> li = new ListItem<T>(newChildId(), i); add(li); onPopulateItem(li); } } super.onBeforeRender(); } public void updateModel() { setModelObject(items); } } |
As you can see there is not much to it.
First there is the familiar onPopulateItem callback which we provide to allow the user to populate rows of the list editor with form components. Because we want to be in complete control over the lifecycle of those components we use RepeatingView as the base class. RepeatingView is the lowest level repeater and does not provide any automatic item management like ListView and other higher-level repeaters.
We want to initialize ListEditor to the state of the model when it first shows up, so before it is rendered for the first time we build the component hierarchy to represent the initial state of the model object list in onBeforeRender.
The last interesting tidbit is the items collection and IFormModelUpdateListener#updateModel(). Because we want updates to model object to be atomic we copy the initial state of the model object into the items list and sync it back in updateModel() which is called only when the form’s model is being updated (ie the type conversion and validation steps have passed for all form components the model contains).
The only missing piece is the ListItem class used in the code above. This is a standard repeater item that provides the added convenience of binding its model to an item at a certain index in the ListEditor#items collection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class ListItem<T> extends Item<T> { public ListItem(String id, int index) { super(id, index); setModel(new ListItemModel()); } private class ListItemModel extends AbstractReadOnlyModel<T> { public T getObject() { return ((ListEditor<T>)ListItem.this.getParent()) .items.get(getIndex()); } } } |
At this point we have a pretty good replacement for the ListView with a bonus that our ListEditor has an almost exact same usage pattern as the ListView. The minor difference being that items are added to the ListEditor by using ListEditor#addItem() instead of adding them directly to the model object list – this is, again, so that the model object list is not modified until the form is submitted. An Add button can be implemented like so:
1 2 3 4 5 6 7 | form.add(new Button("add") { public void onSubmit() { listEditor.addItem(new Phone()); } }.setDefaultFormProcessing(false)); |
The only lesson to take away from here is to call Button#setDefaultFormProcessing(false) on the add button so that the form is not processed since we are not updating the models.
Now to implement a Remove button which will remove rows from the editor without submitting the form, but first a convenience base class for buttons meant to be used inside the ListEditor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | public abstract class EditorButton extends Button { private transient ListItem< ? > parent; public EditorButton(String id) { super(id); } protected final ListItem< ? > getItem() { if (parent == null) { parent = findParent(ListItem.class); } return parent; } protected final List< ? > getList() { return getEditor().items; } protected final ListEditor< ? > getEditor() { return (ListEditor< ? >)getItem().getParent(); } protected void onDetach() { parent = null; super.onDetach(); } |
This class will allow us to find the ListEditor instance instead of having to pass it into the button. And now, the button itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class RemoveButton extends EditorButton { public RemoveButton(String id) { super(id); setDefaultFormProcessing(false); } @Override public void onSubmit() { int idx = getItem().getIndex(); for (int i = idx + 1; i < getItem().getParent().size(); i++) { ListItem< ? > item = (ListItem< ? >)getItem().getParent().get(i); item.setIndex(item.getIndex() - 1); } getList().remove(idx); getEditor().remove(getItem()); } } |
The only trick here is that we have manually decremented the index property of ListItems affected by the removal. We have to do this we are reusing the preserving the component hierarchy and need to keep it in sync with the underlying model.
We now have a fully functional, if not yet fully featured, ListEditor that will work much better then ListView ever could. In the next article we will create buttons that will move rows up and down as well as some handy utilities.
Sample project: listeditor (requires at least WICKET-1.4m4 or snapshot)
[...] http://wicketinaction.com/2008/10/building-a-listeditor-form-component/ [...]
Pingback by D — October 15, 2008 @ 6:57 pm
Example doesn’t work because interface “IFormModelUpdateListener” is not a part of wicket.
Please post complete Example or don’t….
Comment by tom — October 17, 2008 @ 11:33 am
Or you can just be grateful that someone actually writes articles. When was your last article written? Jeez.
Comment by dashorst — October 17, 2008 @ 12:19 pm
@tom: Forgot to mention the article was written against Wicket snapshot. I was hoping m4 would be released before I finished the article but it didn’t happen. You could have tried the latest snapshot yourself before posting…
Comment by ivaynberg — October 17, 2008 @ 6:25 pm
@ivaynberg: well, people don’t usually expect to have to use the last development version to run examples in articles, unless explicitly stated :)
Anyway, thank you for the tips! I’m just starting a project using Wicket, and probably will hit this problem sometime.
obs.: I sure hope 1.4-m4 to be released soon… milestones are somewhat ok, but I’m not that confortable using snapshot versions… ^_^
Comment by Tetsuo — October 19, 2008 @ 4:11 am
Keep em comming Igor.. Really nice reading the articles:)
Comment by Nino — October 19, 2008 @ 10:48 am
@Igor: Very nice article. Right now in the downloadable app you set a CompoundPropertyModel to access a Phone’s variables. How would you set the model if instead of having a List you had a List? I can’t see how to plug in a dynamic model in the case of Strings. Do you mind sharing this? Thanks.
Comment by Frank — February 17, 2009 @ 10:35 pm
@Frank: Sorry frank, I dont undertand what you mean by “instead of having a List you had a List?”…
Comment by ivaynberg — February 17, 2009 @ 11:06 pm
@Igor: Oops, i meant instead of having a List you had a List (which, by the way, might be more common than having a List of an entity).
Imagine your Phone wasn’t composed of areacode + phone + ext, but only of a String, so that Person would have List that represent its phone numbers. Hope it’s clear now!
Comment by Frank — February 17, 2009 @ 11:20 pm
Hmmm… I knew I typed in correctly the first time, it’s Wordpress who is stripping type parameters! I’ll now try in scala-ish syntax:
“instead of having a List[Phone] you had a List[String]“
Comment by Frank — February 17, 2009 @ 11:23 pm
@Frank: Then I suppose you would add a single TextField whose model would be the same as the model of the listitem ( item.getModel() )
Comment by ivaynberg — February 17, 2009 @ 11:44 pm
@Igor: That was my first natural reaction, set the TextField’s model to item.getModel() but it didn’t work – it couldn’t set the model.
This morning I found out it was pretty obvious: the ListItemModel was read-only. So I basically made ListItemModel implement IModel (not extend AbstractReadOnlyModel) and in the setModel I did:
public void setObject(T object) {
((ListEditor)ListItem.this.getParent()).items.set(getIndex(), object);
}
…and it works like a charm. Thanks
Comment by Frank — February 18, 2009 @ 11:44 am
example does not work?
org.apache.wicket.WicketRuntimeException: Unable to create application of class com.wicketinaction.WicketApplication
at org.apache.wicket.protocol.http.ContextParamWebApplicationFactory.createApplication(ContextParamWebApplicationFactory.java:82)
at org.apache.wicket.protocol.http.ContextParamWebApplicationFactory.createApplication(ContextParamWebApplicationFactory.java:49)
at org.apache.wicket.protocol.http.WicketFilter.init(WicketFilter.java:677)
at org.mortbay.jetty.servlet.FilterHolder.doStart(FilterHolder.java:99)
at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:594)
at org.mortbay.jetty.servlet.Context.startContext(Context.java:139)
at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1218)
at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:500)
at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:448)
at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
at org.mortbay.jetty.Server.doStart(Server.java:220)
at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
at runjettyrun.Bootstrap.main(Bootstrap.java:76)
….
Comment by keykubat — February 28, 2009 @ 10:19 pm
Thanks for this great article. This is exactly what I was looking for. Superb.
Comment by Rick — April 23, 2009 @ 7:15 am
Great article!
I was looking how to do this in 1.3.x, and it appears to be much more complicated due to final methods and coding against classes vs. interfaces.
How would you suggest one do this in older versions of Wicket?
Comment by L'engin — June 6, 2009 @ 10:25 am
The only piece that is missing in 1.3.x is the IFormModelUpdateListener, so you will have to manually invoke updateModel() on the ListEditor. There are many options to do this:
Implement IFormModelUpdateListener yourself and call it from a custom form’s overridden process() method.
Put the ListEditor into a FormComponentPanel and call ListEditor.updateModel() from FCP.updateModel()
Add a HiddenField to the form and call ListEditor.updateModel() from HiddenField.updateModel()
Other then that everything else should be portable I believe.
Comment by ivaynberg — June 6, 2009 @ 7:44 pm
Ok, cool!
I used the FormComponentPanel method you suggested. It took a bit (ok a lot) of fumbling with details to get it to work, but now that it’s done, works like a charm. Also, other than a few things (like have to use getParent().iterator() instead of getParent().get(int) in the RemoveButton), most things are portable as you say.
Thanks!
Comment by L'engin — June 8, 2009 @ 10:04 am
hi ivaynberg,
at first i want to thank you for this article. i have the book “wicket in action” and there it isnt explained the same way as it is here and i have the feeling, this solution is nicer than the one in the book. thank you very much!
i have one question. in the ListEditor-class in the method onBeforeRender() you set the items-attribute like this:
items = new ArrayList(getModelObject());
why not doing it like this:
items = getModelObject();
why do you create a new ArrayList-object? is it because the constructor supports the more general interface Collection? if so, why not using Collection as the type for the items-attribute?
just asking because i dont want to miss something. maybe there is something i dont understand.
thank you and keep the good work up. :)
Comment by garz — July 9, 2009 @ 5:43 pm
@garz: Because we want all changes to the list to be comitted atomically when the form is submitted we make a copy of the actual list backing the model.
If we did not do this then operations such as move up and move down, for example, would immediately affect the model object before the form was submitted.
Comment by ivaynberg — July 10, 2009 @ 6:14 am
I had this working fine when everything was on a single page. I also used the above example to make it work with lists of strings.
Then I separated different parts of the form into panels on tabs. Now that it is in a panel the add button does not update its model so always adds an empty string.
I don’t see why it should behave differently. Any ideas?
The hierachy was..
form->listeditor stuff
The hierarchy is now..
form->tabbedpanel->panel->listeditor stuff
Maybe there is another problem with forms separated into panels but wizards do this fine..
Help!
Comment by John — July 24, 2009 @ 5:39 pm
Also, now when I switch tabs any updates get lost unless I have submitted the whole form. This is not very intuitive for the user as the tabs really just split up a very very long form which has many parts that are optional.
Comment by John — July 25, 2009 @ 5:48 pm
@John: Remember, the whole point of the ListEditor was that submit is atomic. The problem, I would guess, is that you use a regular TabbedPanel which uses a regular anchor link to switch tabs. That means the form is not submitted.
There are two ways to solve it:
a) Override tabbedpanel’s newLink() factory and return a SubmitLink instead – this will submit the form between tab switches.
b) Use a javascript tab panel instead so that everything is still kept on the same page.
Comment by ivaynberg — July 30, 2009 @ 6:59 pm
I’m getting this error:
java.lang.UnsupportedOperationException: Model class x.x.x.listeditor.ListEditorItem$ListItemModel does not support setObject(Object)
at org.apache.wicket.model.AbstractReadOnlyModel.setObject(AbstractReadOnlyModel.java:55)
at org.apache.wicket.Component.setDefaultModelObject(Component.java:3052)
at org.apache.wicket.markup.html.form.FormComponent.updateModel(FormComponent.java:1168)
Comment by sam — September 24, 2009 @ 8:18 am
Great article – is there a way to modify it to display the items without text boxes until you click on them, kind of like an example you have in the book?
Thanks,
-jim
Comment by James — March 17, 2010 @ 6:59 pm