在 p:selectOneMenu 中使用带有 null/空值的“请选择”f:selectItem

Posted

技术标签:

【中文标题】在 p:selectOneMenu 中使用带有 null/空值的“请选择”f:selectItem【英文标题】:Using a "Please select" f:selectItem with null/empty value inside a p:selectOneMenu 【发布时间】:2013-06-07 19:09:33 【问题描述】:

我正在从数据库中填充<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;
    

When the first item from the menu represented by &lt;f:selectItem&gt; is selected and the form is submitted then, the value obtained in the getAsObject() method is Select which is the label of &lt;f:selectItem&gt; - the first列表中的项目,这在直觉上是完全不期望的。

&lt;f:selectItem&gt;itemValue 属性设置为空字符串时,它会在getAsObject() 方法中抛出java.lang.NumberFormatException: For input string: "",即使该异常已被精确捕获并为ConverterException 注册。

getAsString()return 语句从

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

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

null 被空字符串替换,但当相关对象为 null 时返回空字符串,进而引发另一个问题,如 here 所示。

如何让这类转换器正常工作?

也尝试了org.omnifaces.converter.SelectItemsConverter,但没有任何区别。

【问题讨论】:

你考虑过这个&lt;f:selectItem itemLabel="Select" noSelectionOption="true" /&gt; 吗? 我在这篇文章之前尝试使用noSelectionOption="true" - 一年前,但它似乎也没有任何区别。 【参考方案1】:
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>

【讨论】:

【参考方案2】:

When the select item value is null, then JSF won't render &lt;option value&gt;, but only &lt;option&gt;.因此,浏览器将提交选项的标签。这在html specification(强调我的)中有明确规定:

value = cdata [CS]

此属性指定控件的初始值。 如果未设置此属性,则初始值设置为 OPTION 元素的内容。

您还可以通过查看 HTTP 流量监视器来确认这一点。您应该会看到正在提交的选项标签。

您需要将选择项值设置为空字符串。然后 JSF 将呈现一个&lt;option value=""&gt;。如果您使用的是转换器,那么当值为null 时,您实际上应该从转换器返回一个空字符串""。这在Converter#getAsString() javadoc(强调我的)中也有明确规定:

getAsString

...

如果值为null,则返回零长度字符串,否则为转换结果

因此,如果您将&lt;f:selectItem itemValue="#null"&gt; 与此类转换器结合使用,则将呈现&lt;option value=""&gt;,并且浏览器将仅提交一个空字符串而不是选项标签。

关于处理空字符串提交值(或null),您实际上应该让您的转换器将此责任委托给required="true" 属性。因此,当传入的valuenull 或空字符串时,您应该立即返回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);

至于您对此的特殊问题,

但是当有问题的对象为空时返回一个空字符串,反过来又会引发另一个问题,如 here 所示。

正如那边回答的那样,这是 Mojarra 中的一个错误,自 OmniFaces 1.8 以来在 &lt;o:viewParam&gt; 中被绕过。因此,如果您至少升级到 OmniFaces 1.8.3 并使用其 &lt;o:viewParam&gt; 而不是 &lt;f:viewParam&gt;,那么您应该不会再受到此错误的影响。

OmniFaces SelectItemsConverter 在这种情况下也应该能正常工作。它为null 返回一个空字符串。

【讨论】:

连同这个转换器,我已经将 OmniFaces 升级到 1.8.1,其中 &lt;o:viewParam&gt; 也可以直观地工作。但是,我通常总是害怕一件事 - 如问题所示,在应用程序范围的 bean 中注入 EJB 是否有害? 不。 EJB(和 CDI 托管 bean,但不是 JSF 托管 bean)使用代理模式。被注入的实际实例只是一个代理。除了在每个方法调用的基础上定位当前可用的实例之外,它什么也不做。鉴于您熟悉 JSF,您可以大致想象它在内部使用 FacesContext.getCurrentInstance(),然后从所需的范围映射中获取 bean,在其上调用方法并最终返回其结果(如果有)。这样的代理实例可以完全是应用程序范围的。它是由容器使用反射自动生成的。 请注意,EJB 和 CDI 实际上并没有以这种方式使用 FacesContext,而是有自己的 EJB 和 CDI 上下文事物(分别为 EJBContextBeanManager)。无论如何,你现在应该得到照片了。 感谢您的澄清。在这个转换器中,我个人更喜欢捕获 NumberFormatException 而不是依赖给定的正则表达式,因为它不能防止当给定数字超出给定类型的范围时 - Long 在这种情况下可能是由恶意用户提供:) 这是一个使用标准 API 的启动示例,但您是对的。我更新了使用 OmniFaces Utils#isNumber() 的答案。【参考方案3】: 如果您想避免选择组件的 null 值,最优雅的方法是使用 noSelectionOption

noSelectionOption="true" 时,转换器甚至不会尝试处理该值。

另外,当您将其与 &lt;p:selectOneMenu required="true"&gt; 结合使用时,当用户尝试选择该选项时,您将收到验证错误。

最后一点,您可以使用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="" />

更多阅读here、here或here

【讨论】:

【参考方案4】:

这对我完全有用:

<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.
    

【讨论】:

【参考方案5】:

除了不完整之外,这个答案已被弃用,因为我在发这篇文章时使用的是 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.
    

&lt;f:selectItem&gt;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>

注意第一个&lt;option&gt; 没有value 属性。

This works as expected bypassing the converter when the first option is selected (even though require is set to false).当itemValue 更改为null 以外的其他值时,它的行为就无法预测(我不明白这一点)。

如果列表中的其他项目设置为非空值并且转换器中接收的项目始终为空字符串(即使选择了另一个选项),则无法选择列表中的其他项目。

另外,当这个空字符串在转换器中解析为Long时,在NumberFormatException被抛出后引起的ConverterException不会报UIViewRoot中的错误(至少应该发生这种情况)。可以在服务器控制台上查看完整的异常堆栈跟踪。

如果有人可以对此有所了解,我会接受答案,如果给出的话。

【讨论】:

【参考方案6】:

你混合了一些东西,我不清楚你想要达到什么目标,但让我们试试

这显然会导致 java.lang.NumberFormatException 被抛出 在它的转换器中。

这没什么明显的。如果 value 为空或 null String,您不检查转换器,您应该检查。在这种情况下,转换器应该返回 null。

为什么它将 Select (itemLabel) 呈现为其值而不是空 字符串(项目值)?

选择必须有选择的东西。如果您不提供空值,则会选择列表中的第一个元素,这不是您所期望的。

只需修复转换器以处理空/空字符串,并让 JSF 将返回的 null 作为不允许的值做出反应。首先调用转换,然后是验证。

我希望能回答你的问题。

【讨论】:

getAsObject() 方法中,我更改了return 语句,例如return StringUtils.isNotBlank(value)&amp;&amp;StringUtils.isNumeric(value)?sharableService.find(Long.parseLong(value)):null;,其中类org.apache.commons.lang.StringUtils 由外部库-Apache Common Lang 保存。它对这个更改工作得很好,required 约束也有效,但为什么 &lt;f:selectItem&gt; 渲染 Select 作为它的值而不是渲染一个空字符串?它是否渲染 itemLabel 而不是渲染 itemValue 不,我在转换器中得到了itemLabel(不是itemValue),这是我没想到的。我在getAsObject() 方法中添加了一个语句System.out.println("value = "+value)。它显示value = Select(即itemLabel)。这是正确的吗?我无法相信这一点。这只发生在嵌入的&lt;f:selectItem&gt; 而不是&lt;f:selectItems&gt; 好的我没写清楚,你在转换器中在itemLabel和itemValue之间转换。所以在一个方向你成为 itemValue,并且必须给它 itemLabel,而在另一个方向你必须为 itemLabel 找到 itemValue。

以上是关于在 p:selectOneMenu 中使用带有 null/空值的“请选择”f:selectItem的主要内容,如果未能解决你的问题,请参考以下文章

p:selectOneMenu在p:面板中保存后无负载正确

有条件地渲染取决于 p:selectOneMenu 值

Primefaces valueChangeListener 或 <p:ajax 侦听器未触发 p:selectOneMenu [重复]

数据表中的primefaces selectonemenu和f:selectonemenu

Primefaces p:selectOneMenu 或 p:selectBooleanCheckbox 不触发更改事件,具体取决于我是不是使用 h:form

PrimeFaces p:selectOneMenu宽度