it-swarm.dev

Usando um "Please select" f: selectItem com valor nulo / vazio dentro de um p: selectOneMenu

Estou preenchendo um <p:selectOneMenu/> Do banco de dados da seguinte maneira.

<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"/>

A opção padrão selecionada, quando esta página é carregada, é

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

O conversor:

@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;
    }
}

Quando o primeiro item do menu representado por <f:selectItem> É selecionado e o formulário é enviado, o value obtido no método getAsObject() é Select que é o rótulo de <f:selectItem> - o primeiro item da lista que intuitivamente não é esperado.

Quando o atributo itemValue de <f:selectItem> É definido como uma sequência vazia, ele lança Java.lang.NumberFormatException: For input string: "" No método getAsObject() mesmo que a exceção seja capturada com precisão e registrado para ConverterException.

De alguma forma, isso parece funcionar, quando a instrução return da getAsString() é alterada de

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

para

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

null é substituído por uma string vazia, mas retornando uma string vazia quando o objeto em questão é null, por sua vez, incorre em outro problema, conforme demonstrado aqui .

Como fazer com que esses conversores funcionem corretamente?

Também tentei com org.omnifaces.converter.SelectItemsConverter, Mas não fez diferença.

26
Tiny

Quando o valor do item selecionado é null, o JSF não renderiza <option value>, Mas apenas <option>. Como conseqüência, os navegadores enviarão o rótulo da opção. Isso está claramente especificado em especificação HTML (ênfase minha):

value = cdata [CS]

Este atributo especifica o valor inicial do controle. Se este atributo não estiver definido, o valor inicial será definido para o conteúdo do elemento OPTION.

Você também pode confirmar isso consultando o monitor de tráfego HTTP. Você deve ver o rótulo da opção sendo enviado.

Você precisa definir o valor do item selecionado como uma sequência vazia. O JSF renderizará um <option value="">. Se você estiver usando um conversor, deverá realmente retornar uma string vazia "" Do conversor quando o valor for null. Isso também está claramente especificado em Converter#getAsString() javadoc (ênfase minha):

getAsString

...

Retorna: uma String de comprimento zero se o valor for nulo , caso contrário, o resultado da conversão

Portanto, se você usar <f:selectItem itemValue="#{null}"> Em combinação com esse conversor, um <option value=""> Será renderizado e o navegador enviará apenas uma string vazia em vez do rótulo da opção.

Quanto a lidar com o valor enviado da string vazia (ou null), você deve deixar seu conversor delegar essa responsabilidade ao atributo required="true". Portanto, quando o value de entrada for null ou uma sequência vazia, você deverá retornar null imediatamente. Basicamente o conversor de entidades deve ser implementado da seguinte forma:

@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);
}

Quanto ao seu problema específico com isso,

mas retornar uma string vazia quando o objeto em questão é nulo, por sua vez, gera outro problema, conforme demonstrado aqui .

Conforme respondido por lá, esse é um bug no Mojarra e ignorado em <o:viewParam> Desde o OmniFaces 1.8. Portanto, se você atualizar para o OmniFaces 1.8.3 e usar o <o:viewParam> Em vez de <f:viewParam>, Não será mais afetado por esse bug.

O OmniFaces SelectItemsConverter também deve funcionar tão bem nessa circunstância. Retorna uma string vazia para null.

30
BalusC
  • Se você deseja evitar valores nulos para o componente selecionado, a maneira mais elegante é usar o noSelectionOption.

Quando noSelectionOption="true", o conversor nem tentará processar o valor.

Além disso, quando você combina isso com <p:selectOneMenu required="true"> você receberá um erro de validação quando o usuário tentar selecionar essa opção.

Um toque final, você pode usar o atributo itemDisabled para deixar claro ao usuário que ele não pode usar esta opção.

<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"/>
  • Agora, se você deseja definir um valor nulo , pode 'enganar' o conversor para retornar um valor nulo, usando

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

Mais leitura aqui , aqui ou aqui

4
yannicuLar

Você está misturando algumas coisas, e não está totalmente claro para mim o que você deseja alcançar, mas vamos tentar

Obviamente, isso faz com que o Java.lang.NumberFormatException seja lançado em seu conversor.

Não há nada óbvio nisso. Você não faz check-in no conversor se o valor estiver vazio ou nulo String, e deveria. Nesse caso, o conversor deve retornar nulo.

Por que renderiza Select (itemLabel) como seu valor e não uma sequência vazia (itemValue)?

O select deve ter algo selecionado. Se você não fornecer um valor vazio, o primeiro elemento da lista seria selecionado, o que não é algo que você esperaria.

Apenas conserte o conversor para trabalhar com cadeias vazias/nulas e deixe o JSF reagir ao retorno null como valor não permitido. A conversão é chamada primeiro, depois vem a validação.

Espero que responda às suas perguntas.

2
Danubian Sailor

Além da incompletude, esta resposta foi preterida, pois eu estava usando o Spring no momento deste post:

Modifiquei o método getAsString() do conversor para retornar uma string vazia em vez de retornar null, quando nenhum objeto Country for encontrado como (além de outras alterações),

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

E <f:selectItem> _ itemValue para aceitar um valor null da seguinte maneira.

<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"/>

Isso gera o seguinte 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>

Antes, o HTML gerado parecia,

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

Observe o primeiro <option> sem atributo value.

Isso funciona como esperado, ignorando o conversor quando a primeira opção é selecionada (mesmo que require esteja definido como false). Quando itemValue é alterado para diferente de null, ele se comporta de maneira imprevisível (eu não entendo isso).

Nenhum outro item da lista pode ser selecionado, se estiver definido como um valor não nulo e o item recebido no conversor for sempre uma string vazia (mesmo que outra opção esteja selecionada).

Além disso, quando essa cadeia vazia é analisada para Long no conversor, o ConverterException causado após o lançamento de NumberFormatException não informa o erro no UIViewRoot (pelo menos isso deve acontecer). O stacktrace de exceção completo pode ser visto no console do servidor.

Se alguém pudesse expor alguma luz sobre isso, eu aceitaria a resposta, se for dada.

1
Tiny