Используя "Пожалуйста, выберите" f: selectItem с нулевым/пустым значением внутри p: selectOneMenu

Я заполняю <p:selectOneMenu/> из базы данных следующим образом.

<p:selectOneMenu id="cmbCountry" 
                 value="#{bean.country}"
                 required="true"
                 converter="#{countryConverter}">

    <f:selectItem itemLabel="Select" itemValue="#{null}"/>

    <f:selectItems var="country"
                   value="#{bean.countries}"
                   itemLabel="#{country.countryName}"
                   itemValue="#{country}"/>

    <p:ajax update="anotherMenu" listener=/>
</p:selectOneMenu>

<p:message for="cmbCountry"/>

По умолчанию выбранная опция, когда эта страница загружена,

<f:selectItem itemLabel="Select" itemValue="#{null}"/>

Конвертер:

@ManagedBean
@ApplicationScoped
public final class CountryConverter implements Converter {

    @EJB
    private final Service service = null;

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        try {
            //Returns the item label of <f:selectItem>
            System.out.println("value = " + value);

            if (!StringUtils.isNotBlank(value)) {
                return null;
            } // Makes no difference, if removed.

            long parsedValue = Long.parseLong(value);

            if (parsedValue <= 0) {
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "Message"));
            }

            Country entity = service.findCountryById(parsedValue);

            if (entity == null) {
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_WARN, "", "Message"));
            }

            return entity;
        } catch (NumberFormatException e) {
            throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "Message"), e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return value instanceof Country ? ((Country) value).getCountryId().toString() : null;
    }
}

Когда выбран первый элемент из меню, представленный <f:selectItem>, и форма отправляется затем, value, полученный в методе getAsObject(), равен Select, который является меткой <f:selectItem> - первой элемент в списке, который интуитивно не ожидается вообще.

Когда атрибут itemValue <f:selectItem> установлен в пустую строку, он выдает java.lang.NumberFormatException: For input string: "" в методе getAsObject(), даже если исключение точно поймано и зарегистрировано для ConverterException.

Кажется, что это работает, когда инструкция return getAsString() изменена с

return value instanceof Country?((Country)value).getCountryId().toString():null;

к

return value instanceof Country?((Country)value).getCountryId().toString():"";

null заменяется пустой строкой, но возвращает пустую строку, когда объект, о котором идет речь, null, в свою очередь, несет другую проблему, как показано здесь.

Как сделать такие преобразователи работоспособными?

Также попытался с org.omnifaces.converter.SelectItemsConverter, но это не имело значения.

Ответы

Ответ 1

Когда значение элемента выбора null, JSF не будет отображать <option value>, а только <option>. Как следствие, браузеры вместо этого отправят эту метку. Это четко указано в HTML-спецификации (выделено мной):

value = cdata [CS]

Этот атрибут определяет начальное значение элемента управления. Если этот атрибут не установлен, начальное значение устанавливается на содержимое элемента OPTION.

Вы также можете подтвердить это, посмотрев на монитор трафика HTTP. Вы должны увидеть отображаемую метку опции.

Вместо этого вам нужно установить значение элемента выбора в пустую строку. Затем JSF отобразит <option value="">. Если вы используете конвертер, вы должны фактически вернуть пустую строку "" из конвертера, когда значение null. Это также четко указано в Converter#getAsString() javadoc (основное внимание):

getAsString

...

Возвращает: строка с нулевой длиной, если значение равно null, в противном случае результат преобразования

Итак, если вы используете <f:selectItem itemValue="#{null}"> в сочетании с таким конвертером, тогда будет отображаться <option value="">, и браузер будет отправлять только пустую строку вместо метки опции.

Что касается обработки пустого значения строки (или null), вы должны позволить своему конвертеру делегировать эту ответственность атрибуту required="true". Итак, когда входящий value равен null или пустой строке, вы должны немедленно вернуть null. В основном ваш конвертер объектов должен быть реализован следующим образом:

@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
    if (value == null) {
        return ""; // Required by spec.
    }

    if (!(value instanceof SomeEntity)) {
        throw new ConverterException("Value is not a valid instance of SomeEntity.");
    }

    Long id = ((SomeEntity) value).getId();
    return (id != null) ? id.toString() : "";
}

@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
    if (value == null || value.isEmpty()) {
        return null; // Let required="true" do its job on this.
    }

    if (!Utils.isNumber(value)) {
        throw new ConverterException("Value is not a valid ID of SomeEntity.");
    }

    Long id = Long.valueOf(value);
    return someService.find(id);
}

Что касается вашей конкретной проблемы с этим,

но возвращая пустую строку, когда объект, о котором идет речь, имеет значение null, в свою очередь несет другую проблему, как показано здесь.

Как уже было сказано, это ошибка в Мохарре и обходится в <o:viewParam> с OmniFaces 1.8. Поэтому, если вы обновляете, по крайней мере, OmniFaces 1.8.3 и используете <o:viewParam> вместо <f:viewParam>, то вам не следует больше влиять на эту ошибку.

OmniFaces SelectItemsConverter также должен хорошо работать в этом случае. Он возвращает пустую строку для null.

Ответ 2

  • Если вы хотите избегать нулевых значений для выбранного компонента, самым элегантным способом является использование noSelectionOption.

Когда noSelectionOption="true", конвертер даже не попытается обработать значение.

Кроме того, при объединении с <p:selectOneMenu required="true"> вы получите ошибку проверки, когда пользователь попытается выбрать эту опцию.

В последнем случае вы можете использовать атрибут itemDisabled, чтобы он дал понять пользователю, что он не может использовать эту опцию.

<p:selectOneMenu id="cmbCountry"
                 value="#{bean.country}"
                 required="true"
                 converter="#{countryConverter}">

    <f:selectItem itemLabel="Select"
                  noSelectionOption="true"
                  itemDisabled="true"/>

    <f:selectItems var="country"
                   value="#{bean.countries}"
                   itemLabel="#{country.countryName}"
                   itemValue="#{country}"/>

    <p:ajax update="anotherMenu" listener=/>
</p:selectOneMenu>

<p:message for="cmbCountry"/>
  • Теперь, если вы хотите, чтобы установить нулевое значение, вы можете "обмануть" конвертер, чтобы вернуть нулевое значение, используя

    <f:selectItem itemLabel="Select" itemValue="" />
    

Подробнее читайте здесь, здесь, или здесь

Ответ 3

Вы смешиваете несколько вещей, и мне не совсем понятно, чего вы хотите достичь, но попробуйте

Это, очевидно, вызывает выброс java.lang.NumberFormatException в преобразователе.

В этом нет ничего очевидного. Вы не проверяете конвертер, если значение пустое или null String, и вы должны. В этом случае преобразователь должен вернуть значение null.

Почему это делает Select (itemLabel) как его значение, а не пустым string (itemValue)?

Выбор должен иметь что-то выбранное. Если вы не укажете пустое значение, будет выбран первый элемент из списка, что не является ожидаемым.

Просто исправьте конвертер для работы с пустыми/нулевыми строками и пусть JSF отреагирует на возвращаемое значение null как недопустимое значение. Сначала вызывается преобразование, затем выполняется проверка.

Я надеюсь, что ответы на ваши вопросы.

Ответ 4

В дополнение к незавершенности этот ответ был устаревшим, поскольку во время этого сообщения я использовал Spring:

Я изменил метод конвертера getAsString(), чтобы вернуть пустую строку вместо возврата null, когда объект Country найден как (в дополнение к некоторым другим изменениям),

@Controller
@Scope("request")
public final class CountryConverter implements Converter {

    @Autowired
    private final transient Service service = null;

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        try {
            long parsedValue = Long.parseLong(value);

            if (parsedValue <= 0) {
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "The id cannot be zero or negative."));
            }

            Country country = service.findCountryById(parsedValue);

            if (country == null) {
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_WARN, "", "The supplied id doesn't exist."));
            }

            return country;
        } catch (NumberFormatException e) {
            throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "Conversion error : Incorrect id."), e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return value instanceof Country ? ((Country) value).getCountryId().toString() : ""; //<--- Returns an empty string, when no Country is found.
    }
}

И <f:selectItem> itemValue, чтобы принять значение null следующим образом.

<p:selectOneMenu id="cmbCountry"
                 value="#{stateManagedBean.selectedItem}"
                 required="true">

    <f:selectItem itemLabel="Select" itemValue="#{null}"/>

    <f:selectItems var="country"
                   converter="#{countryConverter}"
                   value="#{stateManagedBean.selectedItems}"
                   itemLabel="#{country.countryName}"
                   itemValue="${country}"/>
</p:selectOneMenu>

<p:message for="cmbCountry"/>

Это генерирует следующий HTML-код.

<select id="form:cmbCountry_input" name="form:cmbCountry_input">
    <option value="" selected="selected">Select</option>
    <option value="56">Country1</option>
    <option value="55">Country2</option>
</select>

Ранее сгенерированный HTML выглядел как

<select id="form:cmbCountry_input" name="form:cmbCountry_input">
    <option selected="selected">Select</option>
    <option value="56">Country1</option>
    <option value="55">Country2</option>
</select>

Обратите внимание на первый <option> без атрибута value.

Это работает как ожидалось, минуя конвертер при выборе первого параметра (даже если require установлено на false). Когда itemValue изменяется на значение, отличное от null, тогда он ведет себя непредсказуемо (я этого не понимаю).

Никаких других элементов в списке не может быть выбрано, если оно установлено на ненулевое значение, а элемент, полученный в конвертере, всегда является пустой строкой (даже если выбран другой параметр).

Кроме того, когда эта пустая строка анализируется на Long в конвертере, ConverterException, который вызывается после того, как выбрано NumberFormatException, не сообщает об ошибке в UIViewRoot (по крайней мере, это должно произойти). Полное исключение stacktrace можно увидеть на консоли сервера.

Если кто-то мог бы рассказать об этом, я бы принял ответ, если он будет дан.

Ответ 5

Это полностью работает со мной:

<p:selectOneMenu id="cmbCountry"
                 value="#{bean.country}"
                 required="true"
                 converter="#{countryConverter}">

    <f:selectItem itemLabel="Select"/>

    <f:selectItems var="country"
                   value="#{bean.countries}"
                   itemLabel="#{country.countryName}"
                   itemValue="#{country}"/>

    <p:ajax update="anotherMenu" listener=/>
</p:selectOneMenu>

Конвертер

@Controller
@Scope("request")
public final class CountryConverter implements Converter {

    @Autowired
    private final transient Service service = null;

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        if (value == null || value.trim().equals("")) {
            return null;
        }
        //....
        // No change
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return value == null ? null : value instanceof Country ? ((Country) value).getCountryId().toString() : null;
        //**** Returns an empty string, when no Country is found ---> wrong should return null, don't care about the rendering.
    }
}

Ответ 6

public void limparSelecao(AjaxBehaviorEvent evt) {
    Object submittedValue = ((UIInput)evt.getSource()).getSubmittedValue();

    if (submittedValue != null) {
        getPojo().setTipoCaixa(null);
    }
}
<p:selectOneMenu id="tipo"
                 value="#{cadastroCaixaMonitoramento.pojo.tipoCaixa}" 
                 immediate="true"
                 required="true"
                 valueChangeListener="#{cadastroCaixaMonitoramento.selecionarTipoCaixa}">

    <f:selectItem itemLabel="Selecione" itemValue="SELECIONE" noSelectionOption="false"/>

    <f:selectItems value="#{cadastroCaixaMonitoramento.tiposCaixa}" 
                   var="tipo" itemValue="#{tipo}"
                   itemLabel="#{tipo.descricao}" />

    <p:ajax process="tipo"
            update="iten_monitorado"
            event="change" listener="#{cadastroCaixaMonitoramento.limparSelecao}" />
</p:selectOneMenu>