问题
我们为企业应用程序提供了一个基于摆动的前端,现在正在为它实现一个(现在更简单的)JSF/Seam/Richfaces前端。
某些页面包含的字段在编辑时会导致其他字段因此而更改。我们需要立即向用户显示此更改(即他们不必按下按钮或任何东西)。
我已经使用 h:commandButton
和向导致其他字段更改的字段添加onchange="submit()"
成功地实现了这一点。这样,当他们编辑字段时,就会进行表单提交,并因此更新其他字段。
这在功能上工作正常,但特别是当服务器处于很大负载下(经常发生)时,表单提交可能需要很长时间,我们的用户在此期间一直在继续编辑字段,然后在呈现对onchange="submit()"
请求的响应时被还原。
为了解决这个问题,我希望实现以下目标:
- 编辑字段时,如果需要
- ,仅处理该字段,并且仅重新呈现它修改的字段(以便用户在此期间所做的任何其他编辑都不会丢失)。
- 按下按钮后,所有字段将正常处理并重新呈现。
(不稳定)解决方案
好的,我认为首先显示我的页面可能是最简单的。请注意,这只是摘录,某些页面将具有许多字段和许多按钮。
<a4j:form id="mainForm">
...
<a4j:commandButton id="calculateButton" value="Calculate" action="#{illustrationManager.calculatePremium()}" reRender="mainForm" />
...
<h:outputLabel for="firstName" value=" First Name" />
<h:inputText id="firstName" value="#{life.firstName}" />
...
<h:outputLabel for="age" value=" Age" />
<h:inputText id="age" value="#{life.age}">
<f:convertNumber type="number" integerOnly="true" />
<a4j:support event="onchange" ajaxSingle="true" reRender="dob" />
</h:inputText>
<h:outputLabel for="dob" value=" DOB" />
<h:inputText id="dob" value="#{life.dateOfBirth}" styleClass="date">
<f:convertDateTime pattern="dd/MM/yyyy" timeZone="#{userPreference.timeZone}" />
<a4j:support event="onchange" ajaxSingle="true" reRender="age,dob" />
</h:inputText>
...
</a4j:form>
更改 age
的值会导致模型中dob
的值发生变化,反之亦然。我使用 reRender="dob"
和 reRender="age,dob"
来显示模型中更改的值。这工作正常。
我还使用全局队列来确保 AJAX 请求的顺序。
但是,在我单击页面上的其他地方或按 tab 键或其他内容之前,onchange
事件不会发生。当用户在 age
中输入值,然后按 calculateButton
而不单击页面上的其他位置或按 Tab 时,这会导致问题。
onchange
事件似乎首先发生,因为我可以看到dob
的值发生了变化,但是在执行calculateButton
请求时,这两个值随后被还原。
因此,最后,要问一个问题:有没有办法确保在发出calculateButton
请求之前完全更新模型和视图,以便它不会还原它们?为什么自从我使用 AJAX 队列以来还没有发生这种情况?
解决方法
有两种策略可以绕过此限制,但它们都需要在facelet代码中膨胀,这可能会使其他开发人员感到困惑并导致其他问题。
解决方法 1:使用 a4j:支持
此策略如下:
- 将
ajaxSingle="true"
属性添加到calculateButton
。 - 将具有
ajaxSingle="true"
属性的a4j:support
标记添加到firstName
。
第一步确保 calculateButton
不会覆盖 age
或 dob
中的值,因为它不再处理它们。不幸的是,它的副作用是它不再处理firstName
。添加第二步通过在按下calculateButton
之前处理firstName
来抵消这种副作用。
请记住,可能有 20+ 个字段,例如 firstName
.用户填写表单可能会导致 20+ 请求服务器!就像我之前提到的,这也是可能会使其他开发人员感到困惑的膨胀。
解决方法 2:使用进程列表
感谢@DaveMaple和@MaxKatz提出此策略,如下所示:
- 将
ajaxSingle="true"
属性添加到calculateButton
。 - 将
process="firstName"
属性添加到calculateButton
。
第一步实现与第一个解决方法相同的效果,但具有相同的副作用。这一次,第二步确保firstName
在按下时calculateButton
处理。
同样,请记住,此列表中可能包含 20+ 个字段,例如 firstName
。就像我之前提到的,这也是可能会使其他开发人员感到困惑的膨胀,特别是因为列表必须包含某些字段而不是其他字段。
年龄和出生日期设置器和 Getters(以防万一它们是问题的原因)
public Number getAge() {
Long age = null;
if (dateOfBirth != null) {
Calendar epochCalendar = Calendar.getInstance();
epochCalendar.setTimeInMillis(0L);
Calendar dobCalendar = Calendar.getInstance();
dobCalendar.setTimeInMillis(new Date().getTime() - dateOfBirth.getTime());
dobCalendar.add(Calendar.YEAR, epochCalendar.get(Calendar.YEAR) * -1);
age = new Long(dobCalendar.get(Calendar.YEAR));
}
return (age);
}
public void setAge(Number age) {
if (age != null) {
// This only gives a rough date of birth at 1/1/<this year minus <age> years>.
Calendar calendar = Calendar.getInstance();
calendar.set(calendar.get(Calendar.YEAR) - age.intValue(), Calendar.JANUARY, 1, 0, 0, 0);
setDateOfBirth(calendar.getTime());
}
}
public Date getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(Date dateOfBirth) {
if (notEqual(this.dateOfBirth, dateOfBirth)) {
// If only two digits were entered for the year, provide useful defaults for the decade.
Calendar calendar = Calendar.getInstance();
calendar.setTime(dateOfBirth);
if (calendar.get(Calendar.YEAR) < 50) {
// If the two digits entered are in the range 0-49, default the decade 2000.
calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR) + 2000);
} else if (calendar.get(Calendar.YEAR) < 100) {
// If the two digits entered are in the range 50-99, default the decade 1900.
calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR) + 1900);
}
dateOfBirth = calendar.getTime();
this.dateOfBirth = dateOfBirth;
changed = true;
}
}
你的豆子的范围是什么?当按钮被执行时,它是一个新请求,如果你的 Bean 在请求范围内,那么以前的值将消失。
所以,最后,关于一个问题:有没有办法确保在发出 calculateButton 请求之前完全更新模型和视图,以便它不会还原它们?
您可以做的是从启动 ajax 请求时禁用提交按钮,直到 ajax 请求完成。这将有效地防止用户按下提交按钮,直到它解决:
<a4j:support
event="onblur"
ajaxSingle="true"
onsubmit="jQuery('#mainForm\:calculateButton').attr('disabled', 'disabled');"
oncomplete="jQuery('#mainForm\:calculateButton').removeAttr('disabled');" />
这种方法另外有用的一件事是,如果您在发生这种情况时向最终用户显示"ajaxy"图像,以便他们直观地了解正在发生的事情很快就会解决。您也可以在 onsubmit 方法中显示此图像,然后在完成时将其隐藏。
编辑:问题可能只是您需要将进程属性添加到 a4j:commandButton。此属性按 id 指定应参与模型更新阶段的组件:
<a4j:commandButton
id="calculateButton"
value="Calculate"
action="#{illustrationManager.calculatePremium()}"
process="firstName"
reRender="mainForm" />
我可以提供另一种解决方法。
我们应该在 JS 级别有两个标志:
var requestBlocked = false;
var requestShouldBeSentAfterBlock = false;
h:inputText
元素阻止模糊上的 ajax 请求:
<h:inputText id="dob" onblur="requestBlocked = true;" ...
如果requestBlocked
为 false,a4j:commandButton
发送请求:
<a4j:commandButton id="calculateButton"
onkeyup="requestShouldBeSentAfterBlock = requestBlocked;
return !requestBlocked;" ...
如果requestShouldBeSentAfterBlock
为 true,a4j:support
发送请求:
<a4j:support event="onchange"
oncomplete="requestBlocked = false;
if (requestShouldBeSentAfterBlock) {
requestShouldBeSentAfterBlock = false;
document.getElementById('calculateButton').click();
}" ...
由于oncomplete
块在所有需要的元素重新渲染后工作,因此事情将按所需的顺序工作。
看起来原因在getters/setters的某个地方。例如,一种可能性:当没有ajaxSingle=true
时,单击计算按钮。所有值都设置为模型,因此将同时调用setAge
和setDateOfBirth
。并且可能会发生这种情况,以便在setAge
之前调用setDateOfBirth
.
仔细观察setAge
方法,它实际上将日期重置为年初,即使该日期可能已经有正确的年份。
我建议先简化逻辑。例如,为年份和出生日期提供单独的断开连接的字段,并检查问题是否仍然可以重现,以找到重现所需的最小条件。
同样从用户体验的角度来看,常见的做法是执行以下操作,以便在用户停止键入后触发事件:
<a4j:support event="onkeyup"
ajaxSingle="true"
ignoreDupResponses="true"
requestDelay="300"
reRender="..."/>