web-dev-qa-db-fra.com

Comment puis-je remplir un champ de texte en utilisant PrimeFaces AJAX après que des erreurs de validation se soient produites?

J'ai un formulaire dans une vue qui effectue un traitement partiel ajax pour l'autocomplétion et la localisation gmap. Mon backing bean instancie un objet entité "Address" et c'est à cet objet que les entrées du formulaire sont référencées:

@ManagedBean(name="mybean")
@SessionScoped
public class Mybean implements Serializable {
    private Address address;
    private String fullAddress;
    private String center = "0,0";
    ....

    public mybean() {
        address = new Address();
    }
    ...
   public void handleAddressChange() {
      String c = "";
      c = (address.getAddressLine1() != null) { c += address.getAddressLine1(); }
      c = (address.getAddressLine2() != null) { c += ", " + address.getAddressLine2(); }
      c = (address.getCity() != null) { c += ", " + address.getCity(); }
      c = (address.getState() != null) { c += ", " + address.getState(); }
      fullAddress = c;
      addMessage(new FacesMessage(FacesMessage.SEVERITY_INFO, "Full Address", fullAddress));
      try {
            geocodeAddress(fullAddress);
        } catch (MalformedURLException ex) {
            Logger.getLogger(Mybean.class.getName()).log(Level.SEVERE, null, ex);
        } catch (UnsupportedEncodingException ex) {
            Logger.getLogger(Mybean.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(Mybean.class.getName()).log(Level.SEVERE, null, ex);
        } catch (ParserConfigurationException ex) {
            Logger.getLogger(Mybean.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SAXException ex) {
            Logger.getLogger(Mybean.class.getName()).log(Level.SEVERE, null, ex);
        } catch (XPathExpressionException ex) {
            Logger.getLogger(Mybean.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void geocodeAddress(String address)
            throws MalformedURLException, UnsupportedEncodingException,
            IOException, ParserConfigurationException, SAXException,
            XPathExpressionException {

        // prepare a URL to the geocoder
        address = Normalizer.normalize(address, Normalizer.Form.NFD);
        address = address.replaceAll("[^\\p{ASCII}]", "");

        URL url = new URL(GEOCODER_REQUEST_PREFIX_FOR_XML + "?address="
                + URLEncoder.encode(address, "UTF-8") + "&sensor=false");

        // prepare an HTTP connection to the geocoder
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        Document geocoderResultDocument = null;

        try {
            // open the connection and get results as InputSource.
            conn.connect();
            InputSource geocoderResultInputSource = new InputSource(conn.getInputStream());

            // read result and parse into XML Document
            geocoderResultDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(geocoderResultInputSource);
        } finally {
            conn.disconnect();
        }

        // prepare XPath
        XPath xpath = XPathFactory.newInstance().newXPath();

        // extract the result
        NodeList resultNodeList = null;

        // c) extract the coordinates of the first result
        resultNodeList = (NodeList) xpath.evaluate(
                "/GeocodeResponse/result[1]/geometry/location/*",
                geocoderResultDocument, XPathConstants.NODESET);
        String lat = "";
        String lng = "";
        for (int i = 0; i < resultNodeList.getLength(); ++i) {
            Node node = resultNodeList.item(i);
            if ("lat".equals(node.getNodeName())) {
                lat = node.getTextContent();
            }
            if ("lng".equals(node.getNodeName())) {
                lng = node.getTextContent();
            }
        }
        center = lat + "," + lng;
    }

L'auto-complétion et la cartographie des demandes ajax fonctionnent correctement avant de traiter l'intégralité du formulaire lors de la soumission. Si la validation échoue, ajax fonctionne toujours correctement, à l'exception du champ fullAddress qui ne peut pas être mis à jour dans la vue, même lorsque sa valeur est correctement définie sur le bean de sauvegarde après la demande ajax.

<h:outputLabel for="address1" value="#{label.addressLine1}"/>
<p:inputText required="true" id="address1" 
          value="#{mybean.address.addressLine1}">
  <p:ajax update="latLng,fullAddress" 
          listener="#{mybean.handleAddressChange}" 
          process="@this"/>
</p:inputText>
<p:message for="address1"/>

<h:outputLabel for="address2" value="#{label.addressLine2}"/>
<p:inputText id="address2" 
          value="#{mybean.address.addressLine2}" 
          label="#{label.addressLine2}">
  <f:validateBean disabled="#{true}" />
  <p:ajax update="latLng,fullAddress" 
          listener="#{mybean.handleAddressChange}" 
          process="address1,@this"/>
</p:inputText>
<p:message for="address2"/>

<h:outputLabel for="city" value="#{label.city}"/>
<p:inputText required="true" 
          id="city" value="#{mybean.address.city}" 
          label="#{label.city}">
  <p:ajax update="latLng,fullAddress" 
          listener="#{mybean.handleAddressChange}" 
          process="address1,address2,@this"/>
</p:inputText>
<p:message for="city"/>

<h:outputLabel for="state" value="#{label.state}"/>
<p:autoComplete id="state" value="#{mybean.address.state}" 
          completeMethod="#{mybean.completeState}" 
          selectListener="#{mybean.handleStateSelect}"
          onSelectUpdate="latLng,fullAddress,growl" 
          required="true">
  <p:ajax process="address1,address2,city,@this"/>
</p:autoComplete>
<p:message for="state"/> 

<h:outputLabel for="fullAddress" value="#{label.fullAddress}"/>
<p:inputText id="fullAddress" value="#{mybean.fullAddress}" 
          style="width: 300px;"
          label="#{label.fullAddress}"/>
<p:commandButton value="#{label.locate}" process="@this,fullAddress"
          update="growl,latLng" 
          actionListener="#{mybean.findOnMap}" 
          id="findOnMap"/>

<p:gmap id="latLng" center="#{mybean.center}" zoom="18" 
          type="ROADMAP" 
          style="width:600px;height:400px;margin-bottom:10px;" 
          model="#{mybean.mapModel}" 
          onPointClick="handlePointClick(event);" 
          pointSelectListener="#{mybean.onPointSelect}" 
          onPointSelectUpdate="growl" 
          draggable="true" 
          markerDragListener="#{mybean.onMarkerDrag}" 
          onMarkerDragUpdate="growl" widgetVar="map"/>
<p:commandButton id="register" value="#{label.register}" 
          action="#{mybean.register}" ajax="false"/>

Si je rafraîchis la page, les messages d'erreur de validation disparaissent et l'ajax termine le champ fullAddress comme prévu.

Un autre comportement étrange se produit également lors de la validation: j'ai désactivé la validation du bean pour un champ de formulaire, comme le montre le code. Cela fonctionne bien jusqu'à ce que d'autres erreurs de validation soient trouvées, puis, si je soumets à nouveau le formulaire, JSF effectue la validation du bean pour ce champ!

Je suppose que je manque quelque chose pendant l'état de validation, mais je ne peux pas comprendre ce qui ne va pas. Quelqu'un sait-il comment déboguer le cycle de vie JSF? Des idées?

45
Erick Martinez

La cause du problème peut être comprise en considérant les faits suivants:

  • Lorsque la validation JSF réussit pour un composant d'entrée particulier pendant la phase de validation, la valeur soumise est définie sur null et la valeur validée est définie comme valeur locale du composant d'entrée.

  • Lorsque la validation JSF échoue pour un composant d'entrée particulier pendant la phase de validation, la valeur soumise est conservée dans le composant d'entrée.

  • Lorsqu'au moins un composant d'entrée n'est pas valide après la phase de validation, JSF ne mettra à jour les valeurs du modèle pour aucun des composants d'entrée. JSF procédera directement à la phase de réponse du rendu.

  • Lorsque JSF restitue les composants d'entrée, il teste d'abord si la valeur soumise n'est pas null puis l'affiche, sinon si la valeur locale n'est pas null puis l'affiche, sinon elle s'affiche la valeur du modèle.

  • Tant que vous interagissez avec la même vue JSF, vous avez affaire au même état de composant.

Ainsi, lorsque la validation a échoué pour un formulaire particulier, vous devez mettre à jour les valeurs des champs de saisie par une action ajax différente ou même un formulaire ajax différent (par exemple, remplir un champ en fonction d'une sélection déroulante ou du résultat de certains sous forme de boîte de dialogue modale, etc.), vous devez ensuite réinitialiser les composants d'entrée cibles afin que JSF affiche la valeur du modèle qui a été modifiée lors de l'appel. Sinon, JSF affichera toujours sa valeur locale telle qu'elle était lors de l'échec de la validation et les conservera dans un état invalidé.

L'une des façons de votre cas particulier est de collecter manuellement tous les ID des composants d'entrée qui doivent être mis à jour/restitués par PartialViewContext#getRenderIds() puis réinitialiser manuellement son état et les valeurs soumises par EditableValueHolder#resetValue() .

FacesContext facesContext = FacesContext.getCurrentInstance();
PartialViewContext partialViewContext = facesContext.getPartialViewContext();
Collection<String> renderIds = partialViewContext.getRenderIds();

for (String renderId : renderIds) {
    UIComponent component = viewRoot.findComponent(renderId);
    EditableValueHolder input = (EditableValueHolder) component;
    input.resetValue();
}

Vous pouvez le faire dans la méthode d'écoute handleAddressChange(), ou dans une implémentation réutilisable ActionListener que vous attachez en tant que <f:actionListener> = au composant d'entrée qui appelle la méthode d'écoute handleAddressChange().


Pour en revenir au problème concret, j'imagine qu'il s'agit d'un oubli dans la spécification JSF2. Cela aurait beaucoup plus de sens pour nous, développeurs JSF, lorsque la spécification JSF exige ce qui suit:

  • Lorsque JSF doit mettre à jour/restituer un composant d'entrée par une demande ajax et que ce composant d'entrée n'est pas inclus dans le processus/l'exécution de la demande ajax, alors JSF doit réinitialiser la valeur du composant d'entrée.

Cela a été signalé comme JSF issue 106 et une solution complète et réutilisable a été implémentée dans la bibliothèque OmniFaces comme ResetInputAjaxActionListener ( code source ici et démonstration de démonstration ici ).

Mise à jour 1: Depuis la version 3.4, PrimeFaces a basé sur cette idée a également introduit une solution complète et réutilisable dans la saveur de <p:resetInput> .

Mise à jour 2: Depuis la version 4.0, le <p:ajax> a obtenu un nouvel attribut booléen resetValues ce qui devrait également résoudre ce genre de problème sans avoir besoin d'une balise supplémentaire.

Mise à jour 3: JSF 2.2 introduit <f:ajax resetValues> , suivant la même idée que <p:ajax resetValues>. La solution fait maintenant partie de l'API JSF standard.

82
BalusC

Comme l'a expliqué BalusC, vous pouvez également ajouter un écouteur réutilisable qui nettoie toutes les valeurs d'entrée, par exemple:

public class CleanLocalValuesListener implements ActionListener {

@Override
public void processAction(ActionEvent actionEvent) throws AbortProcessingException {
    FacesContext context = FacesContext.getCurrentInstance();
    UIViewRoot viewRoot = context.getViewRoot();
    List<UIComponent> children = viewRoot.getChildren();

    resetInputValues(children);
}

private void resetInputValues(List<UIComponent> children) {
    for (UIComponent component : children) {
        if (component.getChildCount() > 0) {
            resetInputValues(component.getChildren());
        } else {
            if (component instanceof EditableValueHolder) {
                EditableValueHolder input = (EditableValueHolder) component;
                input.resetValue();
            }
        }
    }
  }
}

Et utilisez-le chaque fois que vous avez besoin de nettoyer vos valeurs locales:

<f:actionListener type="com.cacib.bean.CleanLocalValuesListener"/>
3
Ana

À l'intérieur de votre balise <p:ajax/>, veuillez ajouter un attribut resetValues="true" pour dire à la vue de récupérer à nouveau les données, de cette manière devrait être en mesure de résoudre votre problème.

3
BobGao