我在 View 中有一个表单,它执行自动完成和 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 字段无法在 View 中更新之外,ajax 仍然可以正常工作,即使在 ajax 请求之后在支持 bean 上正确设置了它的值。
<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
并将验证后的值设置为输入组件的本地值。当 JSF 在验证阶段对特定输入组件进行验证失败时,提交的值将保留在输入组件中。
当验证阶段后至少有一个输入组件无效时,JSF 将不会更新任何输入组件的模型值。 JSF 将直接进入渲染响应阶段。
当 JSF 渲染输入组件时,它会首先测试提交的值是否不是
null
然后显示它,否则如果本地值不是null
然后显示它,否则显示模型值。只要您与相同的 JSF View 交互,您就会处理相同的组件状态。
因此,当特定表单提交的验证失败并且您恰好需要通过不同的 ajax 操作甚至不同的 ajax 表单来更新输入字段的值时(例如,根据下拉选择或某些模式对话框表单的结果等),那么您基本上需要重置目标输入组件,以便让 JSF 显示在调用操作期间编辑的模型值。否则,JSF 仍将显示验证失败期间的本地值,并使它们保持无效状态。
在您的特定情况中,一种方法是手动收集将由 PartialViewContext#getRenderIds()
更新/重新渲染的输入组件的所有 ID。然后通过 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()
的输入组件监听器方法。
回到具体问题,我认为这是 JSF2 规范中的一个疏忽。当 JSF 规范规定以下内容时,这对我们 JSF 开发人员来说更有意义:
- 当 JSF 需要通过 ajax 请求更新/重新渲染输入组件,并且该输入组件未包含在 ajax 请求的处理/执行中时,JSF 应重置输入组件的值。
这已报告为 JSF issue 1060并在 OmniFaces 中实现了完整且可重用的解决方案。库为 ResetInputAjaxActionListener
(源代码here和展示演示here)。
更新1:从3.4版本开始,PrimeFaces基于这个想法还推出了一个完整的、可重用的解决方案,风格 <p:resetInput>
。
更新 2:自版本 4.0 起, <p:ajax>
获得了一个新的 bool 属性 resetValues
这也应该可以解决此类问题,而不需要额外的标签。
更新 3:引入 JSF 2.2 <f:ajax resetValues>
,遵循与<p:ajax resetValues>
相同的想法。该解决方案现在是标准 JSF API 的一部分。