Skip navigation

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){
        @Override
        protected void populateItem( final ListItem<ProductCustomField> item ) {
            item.add( new CustomFieldRowPanel("fieldRow", item.getModel()){
                // Delete icon was clicked.
                @Override
                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):

 

@Entity
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;
}

This is my way of decomposing an @Entity editing Page in Apache Wicket framework (applies for 1.4, 1.5, 6.x+).

 

Basic principle

Use just one page for all CRUD (create, read, update, delete). It may have drawbacks, but it's the fastest way.

     Create:   create a new entity object (not persistent yet) with default values and use it as Page's model.

     Read:     load the entity from DB

     Update:  use ajax-based editable labels to edit values in place.

     Delete:   Button with ajax-based confirmation somewhere on the page. User can see what he actually deletes. Redirect to some listing of objects of that kind.

 

Tips & Rules

1) Use fully loaded entity as a model. Use LEFT JOIN FETCH to load collections and other LAZY_LOADed parts.

     Do not resort to Open Session in View.

 

SELECT p FROM Product p LEFT JOIN FETCH p.customFields

 

Use PropertyModel. That will keep page object's property and the model the same.

 

this.setDefaultModel( new PropertyModel(this, "product") );

 

Instead of Page's default model, you can use your own, to avoid type casting.

 

 

2) Use PropertyModel for subcomponents.

     Maybe CompoundPropertyModel could be used? (Not sure if limited to Form component or usable for any.)

 

3) Use EditableLabel. Having a "show" page and "edit" page for everything sucks.

    I have created an EditableLabel and EditableLink components which appear as labels, but are editable when clicked or activated by a JavaScript call.

 

4) Use AJAX to update the page. Reloading or clickink a save button (often) sucks.

 

5) If practical, have only one call to dao.update() - in a special method in the Page object.

 

/**
 *  Called when some of sub-components were updated.
 *
 *  @param target  Ajax target, or null if not a result of AJAX.
 */
private void onProductUpdate( AjaxRequestTarget target ) {
    if( target != null )  target.add( this.feedbackPanel );
    try {
        product = productDao.update( product );
        modelChanged();
        this.feedbackPanel.info("Product saved.");
        if( target != null )
            target.appendJavaScript("window.notifyFlash('Product saved.')");
    } catch( Exception ex ){
        this.feedbackPanel.info("Saving product failed: " + ex.toString());
    }
}

 

     Call to this method from other methods in Page.

     Such methods may be overriden onChange-like methods of your components, used as a mechanism to propagate changes to parent components up to the Page.

     Don't forget to call modelChanged() wherever you change Page's model or it's object.

Filter Blog

By date:
By tag: