Wicket is cool and and gives you great power to create AJAXified pages, but may get tricky.

Most of my mistakes in Wicket come from the Models.

What works for pages generated per request, may fail for AJAX, since for AJAX requests, pages are not re-created. (Also, going to the same page doesn't re-create, just uses the same instance.)


Here's how I dealt with a ListView which displays data from a Map<String, SomeEntity>.


Fist, I create an IModel<List<ProductCustomField>> which loads it's content from the actual value of the component's model. (That's quite important as that might change if you code it to.) The implementation is just the basic Model, not LoadableDetachableModel; LDM would load the data at the start of the request, and then use these stalled data even after the map was changed.

Then, ListView is given this model. So, when rendering during an AJAX request, it always uses the latest data.


When creating the item's component, I pass item.getModel() as it's model.


The JPA Entity, actually mapped in @OneToMany Map, must have the map key as it's property. In my case, it's "name".

Hence, to refer to given object in AJAX action handler, I can use item.getModelObject().getName().


The component structure is:


  • ProductPage
    • FeedbackPanel
    • CustomFieldsPanel
      • ListView
        • CustomFieldRowPanel[]

@Entity ProductCustomField


public CustomFieldsPanel( String id, final IModel<Map<String,ProductCustomField>> fieldMapModel, final FeedbackPanel feedbackPanel ) {

    super( id, fieldMapModel );
    this.feedbackPanel = feedbackPanel;

    this.setOutputMarkupId( true ); // AJAX JavaScript code needs to have some id="...".

    IModel<List<ProductCustomField>> listModel = new Model() {
        @Override protected List<ProductCustomField> getObject() {
            Map<String,ProductCustomField> map = (Map) CustomFieldsPanel.this.getDefaultModelObject();
            return new ArrayList(map.values());

    ListView<ProductCustomField> listView;
    add( listView = new ListView<ProductCustomField>("fieldsRows", listModel){
        protected void populateItem( final ListItem<ProductCustomField> item ) {
            item.add( new CustomFieldRowPanel("fieldRow", item.getModel()){
                // Delete icon was clicked.
                protected void onDelete( AjaxRequestTarget target ) {
                    Map<String,ProductCustomField> fieldsMap = (Map) CustomFieldsPanel.this.getDefaultModelObject();
                    fieldsMap.remove( item.getModelObject().getName() );
                    target.add( CustomFieldsPanel.this ); // Update UI.
                    try {
                        CustomFieldsPanel.this.onChange( target ); // Persists.
                    } catch (Exception ex){
                        feedbackPanel.error( ex.toString() );


The entity (simplified):


public class ProductCustomField implements Serializable {

    @Id  @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Basic(optional = false) private String name;

    @Basic(optional = false)  private String label;