我在视图中有一个表单,该表单执行自动完成和gmap本地化的ajax部分处理。我的支持bean实例化了一个实体对象“ Address”,并向该对象引用了表单的输入:
@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; }
自动补全和map ajax请求可以正常工作,然后再提交时处理整个表单。如果验证失败,则即使无法在视图中更新fullAddress字段(即使在ajax请求之后在备用bean上正确设置了其值),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"/>
如果刷新页面,验证错误消息就会消失,并且ajax会按预期完成fullAddress字段。
验证期间还会发生另一种怪异的行为:如代码所示,我已禁用了表单字段的bean验证。在发现其他验证错误之前,此工作正常进行,然后,如果我重新提交表单,JSF将对该字段进行bean验证!
我想我在验证状态中遗漏了一些东西,但是我无法弄清楚它出了什么问题。有谁知道如何调试JSF生命周期?有任何想法吗?
考虑以下事实可以理解问题的原因:
当在验证阶段针对特定输入组件的JSF验证成功时,则将提交的值设置为null,并将验证的值设置为输入组件的本地值。
null
如果在验证阶段对特定输入组件的JSF验证失败,则提交的值将保留在输入组件中。
在验证阶段之后,如果至少一个输入组件无效,那么JSF将不会更新任何输入组件的模型值。JSF将直接进入渲染响应阶段。
当JSF渲染输入组件时,它将首先测试提交的值是否不是null,然后显示它;如果本地值不是null,然后显示它,否则它将显示模型值。
只要您与相同的JSF视图进行交互,就可以处理相同的组件状态。
因此,当针对特定表单提交的验证失败时,您碰巧需要通过不同的ajax动作甚至不同的ajax表单(例如,根据下拉选择或某些结果填充字段)来更新输入字段的值。模态对话框形式等),那么您基本上需要重置目标输入组件,以使JSF可以显示在调用动作期间编辑的模型值。否则,JSF仍将显示其本地值,如在验证失败期间一样,并将其保持在无效状态。
在 您的特定情况下 ,方法之一是手动收集要由更新/重新呈现的输入组件的所有ID,PartialViewContext#getRenderIds()然后由手动重置其状态和提交的值EditableValueHolder#resetValue()。
PartialViewContext#getRenderIds()
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(); }
您可以在handleAddressChange()侦听器方法内部ActionListener,也可以<f:actionListener>在与调用handleAddressChange()侦听器方法的输入组件相连的可重用实现中进行此操作。
handleAddressChange()
ActionListener
<f:actionListener>
回到具体问题,我想这是对JSF2规范的疏忽。当JSF规范强制执行以下命令时,对我们JSF开发人员而言将更加有意义:
这已被报告为JSF问题1060和一个完整的,可重复使用的解决方案已在已实施OmniFaces库作为ResetInputAjaxActionListener(源代码在这里,展示演示在这里)。
ResetInputAjaxActionListener
更新1: 从3.4版开始,PrimeFaces基于此思想还引入了一个完整且可重复使用的解决方案<p:resetInput>。
<p:resetInput>
更新2: 从版本4.0开始,<p:ajax>有一个新的布尔属性resetValues,该属性也可以解决此类问题,而无需其他标签。
<p:ajax>
resetValues
更新3: 引入了JSF 2.2 <f:ajax resetValues>,遵循与相同的想法<p:ajax resetValues>。该解决方案现在是标准JSF API的一部分。
<f:ajax resetValues>
<p:ajax resetValues>