Использование Пожалуйста, выберите 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;

to

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

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

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

Также пробовал с org.omnifaces.converter.SelectItemsConverter, но ничего не изменилось.


person Tiny    schedule 11.06.2013    source источник
comment
Вы обдумывали это <f:selectItem itemLabel="Select" noSelectionOption="true" /> ?   -  person Hatem Alimam    schedule 20.06.2014
comment
Я пытался использовать noSelectionOption="true" до этого поста - год назад, но это тоже не имело значения.   -  person Tiny    schedule 20.06.2014


Ответы (5)


Когда значение выбранного элемента равно 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, в свою очередь возникает другая проблема, как показано здесь.< /эм>

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

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

person BalusC    schedule 20.06.2014
comment
Вместе с этим конвертером я обновил OmniFaces до 1.8.1 в котором <o:viewParam> также работает интуитивно. Однако я всегда опасался одного в целом: не вредно ли вводить EJB в bean-компонент приложения, как показано в вопросе? - person Tiny; 20.06.2014
comment
Неа. Компоненты EJB (и управляемые компоненты CDI, но не управляемые компоненты JSF) используют шаблон прокси. Внедряемый экземпляр — это просто прокси. Он не делает ничего, кроме поиска доступного в данный момент экземпляра для каждого вызова метода. Учитывая, что вы знакомы с JSF, вы можете представить его примерно так, как если бы FacesContext.getCurrentInstance() внутренне использовалось, а затем брался bean-компонент из нужной карты области действия, вызывал для него метод и, наконец, возвращал его результат, если таковой имеется. Такой прокси-экземпляр может быть идеально привязан к приложению. Он автоматически генерируется контейнером с использованием отражения. - person BalusC; 21.06.2014
comment
Обратите внимание, что EJB и CDI на самом деле не используют FacesContext таким образом, но имеют свои собственные контексты EJB и CDI (EJBContext и BeanManager соответственно). Во всяком случае, вы должны получить картину сейчас. - person BalusC; 21.06.2014
comment
Спасибо за это разъяснение. В этом конвертере я лично предпочитаю ловить NumberFormatException вместо того, чтобы полагаться на данное регулярное выражение, потому что оно не защищает от того, когда заданное число выходит за пределы диапазона данного типа — в данном случае Long, которое может быть предоставлено вредоносным Пользователь :) - person Tiny; 22.06.2014
comment
Это был начальный пример с использованием стандартных API, но вы правы. Я обновил ответ, чтобы использовать OmniFaces Utils#isNumber(). - person BalusC; 24.06.2014
comment
Возврат null раньше работал с Mojarra 2.1.28. Когда это изменилось? - person Demonblack; 03.07.2018

  • Если вы хотите избежать нулевых значений для выбранного компонента, самый элегантный способ — использовать класс 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="" />
    

Подробнее здесь, здесь или здесь

person yannicuLar    schedule 26.06.2014

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

Это, очевидно, приводит к тому, что в его конвертере возникает исключение java.lang.NumberFormatException.

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

Почему он отображает Select (itemLabel) как свое значение, а не пустую строку (itemValue)?

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

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

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

person Danubian Sailor    schedule 11.06.2013
comment
Внутри метода getAsObject() я изменил оператор return, например, return StringUtils.isNotBlank(value)&&StringUtils.isNumeric(value)?sharableService.find(Long.parseLong(value)):null;, где класс org.apache.commons.lang.StringUtils хранится во внешней библиотеке — Apache Common Lang. С этим изменением все работало нормально, ограничение required тоже работало, но почему <f:selectItem> отображает Select в качестве своего значения, а не отображает пустую строку? Отрисовывает ли он itemLabel вместо itemValue? - person Tiny; 12.06.2013
comment
Нет, я получаю itemLabel (не itemValue) в конвертере, чего я не ожидаю. Я поместил оператор в метод getAsObject(), System.out.println("value = "+value). Он отображает value = Select (т.е. itemLabel). Это правильно? Я не могу поверить в это. Это происходит только со встроенным <f:selectItem>, а не с <f:selectItems>. - person Tiny; 13.06.2013
comment
ОК, я не ясно написал, вы конвертируете между itemLabel и itemValue в конвертере. Таким образом, в одном направлении вы становитесь itemValue и должны указать для него itemLabel, а в другом направлении вы должны найти itemValue для itemLabel. - person Danubian Sailor; 14.06.2013

В дополнение к неполноте, этот ответ устарел, так как я использовал 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.
    }
}

И itemValue <f:selectItem> принимает значение 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 (по крайней мере, это должно произойти). Полную трассировку стека исключений можно увидеть на консоли сервера.

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

person Tiny    schedule 18.07.2013

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

<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.
    }
}
person Nassim MOUALEK    schedule 19.06.2014