Wicket in Action

Building a ListEditor form component

15 October 2008, by ivaynberg

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 public abstract class ListEditor<T> extends RepeatingView 
 2                              implements IFormModelUpdateListener
 3 {
 4     List<T> items;
 5 
 6     public ListEditor(String id, IModel<List<T>> model)
 7     {
 8         super(id, model);
 9     }
10 
11     protected abstract void onPopulateItem(ListItem<T> item);
12 
13     public void addItem(T value)
14     {
15         items.add(value);
16         ListItem<T> item = new ListItem<T>(newChildId(), 
17                                               items.size() - 1);
18         add(item);
19         onPopulateItem(item);
20     }
21 
22     protected void onBeforeRender()
23     {
24         if (!hasBeenRendered())
25         {
26             items = new ArrayList<T>(getModelObject());
27             for (int i = 0; i < items.size(); i++)
28             {
29                 ListItem<T> li = new ListItem<T>(newChildId(), i);
30                 add(li);
31                 onPopulateItem(li);
32             }
33         }
34         super.onBeforeRender();
35     }
36 
37     public void updateModel()
38     {
39         setModelObject(items);
40     }
41 }

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 public class ListItem<T> extends Item<T>
 2 {
 3    public ListItem(String id, int index)
 4    {
 5       super(id, index);
 6       setModel(new ListItemModel());
 7    }
 8 
 9    private class ListItemModel extends AbstractReadOnlyModel<T>
10    {
11       public T getObject()
12       {
13          return ((ListEditor<T>)ListItem.this.getParent())
14                   .items.get(getIndex());
15       }
16    }
17 }

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 form.add(new Button("add")
2         {
3             public void onSubmit()
4             {
5                 listEditor.addItem(new Phone());
6             }
7         }.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 public abstract class EditorButton extends Button
 2 {
 3     private transient ListItem< ? > parent;
 4 
 5     public EditorButton(String id)
 6     {
 7         super(id);
 8     }
 9 
10     protected final ListItem< ? > getItem()
11     {
12         if (parent == null)
13         {
14             parent = findParent(ListItem.class);
15         }
16         return parent;
17     }
18 
19     protected final List< ? > getList()
20     {
21         return getEditor().items;
22     }
23 
24     protected final ListEditor< ? > getEditor()
25     {
26         return (ListEditor< ? >)getItem().getParent();
27     }
28 
29     protected void onDetach()
30     {
31         parent = null;
32         super.onDetach();
33     }

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 public class RemoveButton extends EditorButton
 2 {
 3 
 4     public RemoveButton(String id)
 5     {
 6         super(id);
 7         setDefaultFormProcessing(false);
 8     }
 9 
10     @Override
11     public void onSubmit()
12     {
13         int idx = getItem().getIndex();
14 
15         for (int i = idx + 1; i < getItem().getParent().size(); i++)
16         {
17             ListItem< ? > item = (ListItem< ? >)getItem().getParent().get(i);
18             item.setIndex(item.getIndex() - 1);
19         }
20 
21         getList().remove(idx);
22         getEditor().remove(getItem());
23     }
24 }

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)

-->