validationjsfconvertersmojarraselectmanylistbox

JSF SelectManyListbox with noSelectionOption="true" - Validation Error: Value is not valid


I have a problem with h:selectManyListbox, when the items are populated with POJO's and the noSelectionOption is true (for h:selectManyListbox with enums as items, it works as I expected).

Bean

@Named
@ViewScoped
public class MyBean implements Serializable {

    private static final long serialVersionUID = 1L;

    private List<BaseDTO> availableItems = null;

    private String[] selectedItems = null;

    @PostConstruct
    private void initialize() {
        loadAvailableItems();
    }

    private void loadAvailableItems() {
        availableItems = Arrays.asList(new BaseDTO("entityId", "entityDescription"), new BaseDTO(...), ...);
    }

    public List<BaseDTO> getAvailableItems() {
        return availableItems;
    }
    
    public String[] getSelectedItems() {
        return selectedItems;
    }

    public void setSelectedItems(String[] selectedItems) {
        this.selectedItems = selectedItems;
    }

}

BaseDTO

public class BaseDTO {

    private String id;

    private String description;

    public BaseDTO(String id, String description) {
        this.id = id;
        this.description = description;
    }

    public String getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }

    @Override
    public String toString() {
        return id;
    }
    
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + id.hashCode();
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        BaseDTO other = (BaseDTO) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }

}

XHTML

<h:selectManyListbox value="#{myBean.selectedItems}" hideNoSelectionOption="false" size="4">
    <f:selectItem itemValue="#{null}" itemLabel="--" noSelectionOption="true" />
    <f:selectItems value="#{myBean.availableItems}" var="entry" itemValue="#{entry.id}" itemLabel="#{entry.description}" />
</h:selectManyListbox>

When I try to submit the page I always get Validation Error: Value is not valid. If I remove the hideNoSelectionOption and the correponding <f:selectItem itemValue="#{null}" itemLabel="--" noSelectionOption="true" /> everything works fine, however I really would like to have this noSelectionOption on my list.

I tried using OmniFaces SelectItemsConverter and even creating my own custom converter, but with no luck. No matter what I try I cannot overcome this validation error.

Meanwhile I found a not so nice workaround:

If my availableItems variable is a Map<String, String> instead of a List:

private Map<String, String> availableItems = null;

and if I add a null entry to the map:

    private void loadAvailableItems() {
        List<BaseDTO> dtoList = Arrays.asList(new BaseDTO("entityId", "entityDescription"));
        availableItems = dtoList.stream().collect(Collectors.toMap(BaseDTO::getId, BaseDTO::getDescription));
        availableItems.put(null, "--");
    }

then, everything works as expected, except the noSelectionOption is not preselected on the page.

I this the expected component behaviour, or am I missing something?

Thanks in advance for your help!


Solution

  • First of all, the noSelectionOption/hideNoSelectionOption attribute pair has been misunderstood here. Please remove them. It has completely no use in your context. In order to better understand their originally intended purpose, head to the answer of Best way to add a "nothing selected" option to a selectOneMenu in JSF, which is summarized as follows:

    The primary purpose of this attribute pair is to prevent the web site user from being able to re-select the "no selection option" when the component has already a non-null value selected.

    In your specific case, you have a multi-select listbox. It makes in first place no sense to have a "nothing selected" option in such user interface element. You simply need to deselect everything in order to have a "nothing selected" state. This isn't possible in for example a single-select dropdown, because you cannot deselect the selected option in first place. Hence such user interface element has the need for a "nothing selected" option. But, again, this isn't needed for a multi-select listbox. I do however understand that it's useful to have an actionable element which automagically deselects everything within the listbox. This could have been done via a link or button somewhere near the listbox.

    In any case, I've been able to reproduce the described issue in Mojarra 2.3.17. The root problem is that the "empty string submitted value" isn't represented by an empty string array anymore, but by a string array with a single item, an empty string. Therefore all checks related to "empty string submitted value" failed afterwards. I don't think that this is a bug in JSF itself, but that it's just a case of unexpected usage of a multi-select component.

    You could work around this all by explicitly disabling the item during all phases other than the render response phase (the 6th phase). It'll be selectable but be automatically removed from the selected items as built-in measure against tampered requests. This way the "empty string submitted value" will be an empty array as expected.

    <h:selectManyListbox value="#{myBean.selectedItems}" size="4">
        <f:selectItem itemValue="#{null}" itemLabel="--" itemDisabled="#{facesContext.currentPhaseId.ordinal ne 6}" />
        <f:selectItems value="#{myBean.availableItems}" var="entry" itemValue="#{entry.id}" itemLabel="#{entry.description}" />
    </h:selectManyListbox>
    

    Noted should be that this cannot be solved with a custom converter or validator. JSF would not allow the custom converter to have returned null, and this specific "Value is not valid" validation is done by built-in validator against tampered requests which cannot be replaced/disabled. Our best bet would probably have been to change/respecify the behavior of noSelectionOption="true" as this is indeed way too often misunderstood. It should probably internally be treated the same way as a disabled item.