Wicket "nullable" component view

1k Views Asked by At

Quite frequently, I'm using custom Wicket components to render model objects. Objects can sometimes be null, in that case a specific div is displayed. In the component HTML rendering code, I thus have two div's, one for the "null" case, and one for the "non-null" case, with some other inner markup. One is displayed while the other is masked.

<div wicket:id="toDisplayWhenObjectIsNull">
    ...
</div>
<div wicket:id="toDisplayWhenObjectIsNotNull">
   <span wicket:id="label">...</span>
   <table wicket:id="table">...</table>
   ...
</div>

The problem I face is that Wicket force me to entirely build the two div, even if the model object is null. In all calls to sub-components building (labels, tables, etc...) I have to check for nullness, which is cumbersome and error-prone:

X myX = getModel().getModelObject();
Label label = new Label("label",
    myX == null ? null : formatY(myX.getY()));

The first solution to this would be to split the non-null part in a specific wicket sub-component, either as it's own class or an inner class of the master component; and inserting this component in place of the "non-null" div. But that double the number of needed files (resources, HTML, java code). This is not ideal.

The second solution, generic, would be to create a "decorator" component to encapsulate any other component, and check for nullness on it's model object. If the component is null, then it would display a standard div, and if not, it would rely on the decorated component. I tried to implement this using borders or composite panels, but I can't manage to make it work. What I would like to achieve is something like this:

// Client code, Java
ViewXPanel xpanel = new ViewXPanel("xpanel", new Model<X>(x));
add(xpanel);

// HTML
<div wicket:id="xpanel"/>

OR, if necessary, make the client responsible of "nullability" of the displayed component, using something like this in the client code:

// Client code, Java
ViewXPanel xpanel = new NullableDecorator(?, ViewXPanel(...));
add(xpanel);
3

There are 3 best solutions below

0
On BEST ANSWER

Well, one workaround is effectively using fragments, as suggested by biziclop, so I'm describing it down here for reference, but not flagging it as an "answer".

ViewX.html:

<wicket:panel ...>
  <div wicket:id="mainPanel"></div>
  <wicket:fragment wicket:id="nullFragment">
    ...markup for NULL case...
  </wicket:fragment>
  <wicket:fragment wicket:id="nonNullFragment">
    <h4><span wicket:id="theName"></span></h4>
    ...other markup for non-NULL case...
  </wicket:fragment>
</wicket:panel

ViewX.java:

public class ViewX extends Panel {
  public ViewX(String id, IModel<X> xmodel) {
    if (x == null) {
      Fragment nullFragment = new Fragment("mainPanel", "nullFragment", null);
      ... eventual markup if needed ...
      add(nullFragment);
    } else {
      Fragment nonNullFragment = new Fragment("mainPanel", "nonNullFragment", null);
      nonNullFragment.add(new Label("theName", new PropertyModel(xmodel, "name")));
      ... other markup ...
      add(nonNullFragment);
} } }

I'm still looking for a really generic solution to this, either by composition, inheritance or decoration on a non-null-aware ViewX panel.

2
On

You're not using the recommended Wicket patterns :(.

Instead of pulling something out of the model and shoving it into another component:

X myX = getModel().getModelObject();
Label label = new Label("label", myX == null ? null : formatY(myX.getY()));

... use models the right way:

Label label = new Label("label", new AbstractReadonlyModel<Y>() {
    public Y getObject() {
        return formatY(getModel().getModelObject().getY());
    }
});

Always do it this way.

No need to test for null apart from switching between "toDisplayWhenObjectIsNull" and "toDisplayWhenObjectIsNotNull" on a top level of your component hierarchy.

0
On

What I did in order to be able to display null values was that I made my own Label subclass, in which I override onComponentTagBody() to replace the empty string with a placeholder:

/**
 * Since the converter is not invoked for null values, override this so that 
 * empty string can be replaced with null display value.
 */
@Override
public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag)
{
    String value = getDefaultModelObjectAsString();
    replaceComponentTagBody(markupStream, openTag, 
                            value.isEmpty()? "N/A": value);
}

This will make it possible to use

// Client code, Java
add(new MyLabel("label");

// HTML
<div wicket:id="label"/>

You will of course have to customize with your preferences for localization and such, but I hope you get the big picture from the provided example.