¿Cómo puedo completar un campo de texto usando PrimeFaces AJAX después de que ocurren errores de validación?
Tengo un formulario en una vista que realiza un procesamiento parcial ajax para autocompletado y localización de gmap. Mi bean de respaldo crea una instancia de un objeto de entidad "Dirección" y es a este objeto al que se hacen referencia las entradas del formulario:
@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;
}
Las solicitudes de autocompletado y map ajax funcionan bien antes de procesar todo el formulario al enviarlo. Si la validación falla, ajax aún funciona bien, excepto el campo fullAddress que no se puede actualizar en la vista, incluso cuando su valor está configurado correctamente en el bean de respaldo después de la solicitud de 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 actualizo la página, los mensajes de error de validación desaparecen y el ajax completa el campo fullAddress como se esperaba.
Otro comportamiento extraño ocurre también durante la validación: he deshabilitado la validación de beans para un campo de formulario, como se ve en el código. Esto funciona bien hasta que se encuentren otros errores de validación; luego, si vuelvo a enviar el formulario, ¡JSF realiza una validación de beans para este campo!
Supongo que me falta algo durante el estado de validación, pero no puedo entender qué tiene de malo. ¿Alguien sabe cómo depurar el ciclo de vida de JSF? ¿Algunas ideas?
La causa del problema se puede entender considerando los siguientes hechos:
Cuando la validación JSF tiene éxito para un componente de entrada en particular durante la fase de validaciones, el valor enviado se establece en
null
y el valor validado se establece como valor local del componente de entrada.Cuando la validación JSF falla para un componente de entrada en particular durante la fase de validaciones, el valor enviado se mantiene en el componente de entrada.
Cuando al menos un componente de entrada no es válido después de la fase de validaciones, JSF no actualizará los valores del modelo para ninguno de los componentes de entrada. JSF procederá directamente a la fase de respuesta.
Cuando JSF representa componentes de entrada, primero probará si el valor enviado no lo es
null
y luego lo mostrará; de lo contrario, si el valor local no lo esnull
y luego lo mostrará; de lo contrario, mostrará el valor del modelo.Mientras interactúes con la misma vista JSF, estarás tratando con el mismo estado de componente.
Entonces, cuando la validación falla para el envío de un formulario en particular y necesita actualizar los valores de los campos de entrada mediante una acción ajax diferente o incluso un formulario ajax diferente (por ejemplo, completar un campo dependiendo de una selección desplegable o el resultado de algún formulario de diálogo modal, etc.), entonces básicamente necesita restablecer los componentes de entrada de destino para que JSF muestre el valor del modelo que se editó durante la acción de invocación. De lo contrario, JSF seguirá mostrando su valor local tal como estaba durante el error de validación y lo mantendrá en un estado invalidado.
Una de las formas en su caso particular es recopilar manualmente todos los ID de los componentes de entrada que deben actualizarse o volver a representarse PartialViewContext#getRenderIds()
y luego restablecer manualmente su estado y los valores enviados 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();
}
Puede hacer esto dentro del handleAddressChange()
método de escucha o dentro de una ActionListener
implementación reutilizable que adjunta como <f:actionListener>
al componente de entrada que llama al handleAddressChange()
método de escucha.
Volviendo al problema concreto, me imagino que se trata de un descuido en la especificación JSF2. Tendría mucho más sentido para nosotros, los desarrolladores de JSF, cuando la especificación JSF exija lo siguiente:
- Cuando JSF necesita actualizar/volver a representar un componente de entrada mediante una solicitud ajax, y ese componente de entrada no está incluido en el proceso/ejecución de la solicitud ajax, entonces JSF debe restablecer el valor del componente de entrada.
Esto se informó como el problema 1060 de JSF y se implementó una solución completa y reutilizable en la biblioteca OmniFacesResetInputAjaxActionListener
como (código fuente aquí y demostración aquí ).
Actualización 1: Desde la versión 3.4, PrimeFaces se ha basado en esta idea y también ha introducido una solución completa y reutilizable en forma de <p:resetInput>
.
Actualización 2: desde la versión 4.0, <p:ajax>
obtuve un nuevo atributo booleano resetValues
que también debería resolver este tipo de problema sin la necesidad de una etiqueta adicional.
Actualización 3: se introdujo JSF 2.2 <f:ajax resetValues>
, siguiendo la misma idea que <p:ajax resetValues>
. La solución ahora forma parte de la API JSF estándar.
Como explicó BalusC, también puedes agregar un oyente reutilizable que limpie todos los valores de entrada, por ejemplo:
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();
}
}
}
}
}
Y úselo siempre que necesite limpiar sus valores locales:
<f:actionListener type="com.cacib.bean.CleanLocalValuesListener"/>
Dentro de su etiqueta <p:ajax/>
, agregue un atributo resetValues="true"
para indicarle a la vista que recupere datos nuevamente, de esta manera debería poder solucionar su problema.