Wednesday, June 6, 2012

Getting PrimeFaces mix of Selects, Objects and Converters to work

If you are having problems with the AJAX Select components from PrimeFaces when selecting Java objects, inside a DataTable or not, this solution might work for you.

If you have something like the following:
<p:selectOneMenu value="#{deviceBean.currentLocation}"
                 converter="locationConverter">

    <f:selectItem itemLabel="#{deviceBean.currentLocation.name}"
                     itemValue="#{deviceBean.currentLocation}" />
    <f:selectItems
        value="#{deviceBean.locations}"
        var="location"
        itemLabel="#{location.place}"
        itemValue="#{location}" />
    <f:ajax event="change" execute="@this" render="@this" />

</p:selectOneMenu>

Your managed bean is something like my DeviceBean:
@ManagedBean
@SessionScoped
public class DeviceBean implements Serializable {

    @EJB
    private Backend backend;

    private List locations;
    private Location currentLocation;

    public DeviceBean() {
        locations = backend.getLocations();
    }

    public List getCurrentLocation() {
        return currentLocation;
    }

    public Location getCurrentLocation() {
        return currentLocation;
    }
}

Your converter is something like:
@FacesConverter(forClass=Location.class,value="locationConverter")
public class LocationConverter implements Converter {

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        Location location = new Location()
        // some operations to set the parameters of Location based on the String
        return location;
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        Location location = (Location) value;
        String string;
        // some operations to save the parameters of Location into a String
        return string;
    }
}

And the object you want to change is something like my Location:
public class Location {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}


And your Select doesn't work, e.g. it doesn't respond to the accept button nor gives any error or logging, then probably it's just discarding the new element you just selected (in my case, of type Location).

It discards it because the object returned by the converter's getAsObject method may not match any of the objects allowed in the locations list from the DeviceBean. If this is the cause, then it means Location doesn't have proper hashCode and equals methods, which are required by Java to check if two objects of the same type have different contents or not, among other things. Because the selectOneMenu requires an object that is on the list, as defined in value="#{deviceBean.currentLocation}", it just fails to do anything.

So, don't forget, get hashCode and equals to the data structures you need to check for equality, explicitily or not, like this (as generated by my NetBeans):
public class Location {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == null)
            return false;
        if(getClass() != obj.getClass())
            return false;
        final LocationTest other = (LocationTest)obj;
        if(!Objects.equals(this.name, other.name))
            return false;
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 83 * hash + Objects.hashCode(this.name);
        return hash;
    }
}