使用 a4j:support 更新模型和视图,为下一个按钮/提交操作做好准备
问题
我们有一个基于 Swing 的企业应用程序前端,现在正在为其实现一个(目前更简单的)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
而不单击页面上的其他位置或按选项卡时,这会导致问题。
onchange
事件似乎首先发生,因为我可以看到 dob
的值发生变化,但是当 calculateButton
请求发生时,这两个值会恢复执行。
所以,最后,问题是:有没有办法确保模型和视图在发出 calculateButton
请求之前完全更新,这样就不会恢复它们?为什么我使用 AJAX 队列后还没有发生这种情况?
解决方法
有两种策略可以解决这个限制,但是它们都需要 Facelet 代码膨胀,这可能会让其他开发人员感到困惑并导致其他问题。
解决方法 1:使用 a4j:support
此策略如下:
- 将
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
。正如我之前提到的,这也是一种臃肿,可能会让其他开发人员感到困惑,特别是因为列表必须包含某些字段,但不包含其他字段。
年龄和出生日期设置器和获取器(以防万一它们是问题的原因)
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;
}
}
The Problem
We have a swing based front end for an enterprise application and now are implementing a (for now simpler) JSF/Seam/Richfaces front end for it.
Some of the pages include fields that, when edited, should cause other fields to change as a result. We need this change to be shown to the user immediately (i.e. they should not have to press a button or anything).
I have implemented this successfully using h:commandButton
and by adding onchange="submit()"
to the fields that cause other fields to change. That way, a form submit occurs when they edit the field, and the other fields are updated as a result.
This works fine functionally, but especially when the server is under significant load (which happens often) the form submits can take a long time and our users have been continuing to edit fields in the meantime which then get reverted when the responses to the onchange="submit()"
requests are rendered.
To solve this problem, I was hoping to achieve something where:
- Upon editing the field, if required, only that field is processed and only the fields it modifies are re-rendered (so that any other edits the user has made in the meantime do not get lost).
- Upon pressing a button, all fields are processed and re-rendered as normal.
The (Unstable) Solution
Okay, I think it might be easiest to show a bit of my page first. Note that this is only an excerpt and that some pages will have many fields and many buttons.
<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>
Changing the value of age
causes the value of dob
to change in the model and vice versa. I use reRender="dob"
and reRender="age,dob"
to display the changed values from the model. This works fine.
I am also using the global queue to ensure ordering of AJAX requests.
However, the onchange
event does not occur until I click somewhere else on the page or press tab or something. This causes problems when the user enters a value in say, age
, and then presses calculateButton
without clicking somewhere else on the page or pressing tab.
The onchange
event does appear to occur first as I can see the value of dob
change but the two values are then reverted when the calculateButton
request is performed.
So, finally, to the question: Is there a way to ensure that the model and view are updated completely before the calculateButton
request is made so that it does not revert them? Why is that not happening already since I am using the AJAX queue?
The Workarounds
There are two strategies to get around this limitation but they both require bloat in the facelet code which could be confusing to other developers and cause other problems.
Workaround 1: Using a4j:support
This strategy is as follows:
- Add the
ajaxSingle="true"
attribute tocalculateButton
. - Add the
a4j:support
tag with theajaxSingle="true"
attribute tofirstName
.
The first step ensures that calculateButton
does not overwrite the values in age
or dob
since it no longer processes them. Unfortunately it has the side effect that it no longer processes firstName
either. The second step is added to counter this side effect by processing firstName
before calculateButton
is pressed.
Keep in mind though that there could be 20+ fields like firstName
. A user filling out a form could then cause 20+ requests to the server! Like I mentioned before this is also bloat that may confuse other developers.
Workaround 2: Using the process list
Thanks to @DaveMaple and @MaxKatz for suggesting this strategy, it is as follows:
- Add the
ajaxSingle="true"
attribute tocalculateButton
. - Add the
process="firstName"
attribute tocalculateButton
.
The first step achieves the same as it did in the first workaround but has the same side effect. This time the second step ensures that firstName
is processed with calculateButton
when it is pressed.
Again, keep in mind though that there could be 20+ fields like firstName
to include in this list. Like I mentioned before this is also bloat that may confuse other developers, especially since the list must include some fields but not others.
Age and DOB Setters and Getters (just in case they are the cause of the issue)
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;
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
你的bean的范围是什么?当按钮被执行时,这是一个新的请求,如果您的 bean 在请求范围内,那么以前的值将消失。
What is the scope of your bean? When the button is executed, it's a new request and if your bean is in request scope, then previous values will be gone.
您可以做的是从您发起 ajax 请求到 ajax 请求完成期间禁用提交按钮。这将有效地防止用户按下提交按钮,直到问题解决:
此方法另外有帮助的一件事是,如果您在发生这种情况时向最终用户显示“ajaxy”图像,以便他们直观地了解正在发生的事情这很快就会解决。您也可以在 onsubmit 方法中显示此图像,然后在完成时隐藏它。
编辑:
问题可能只是您需要将 process 属性添加到 a4j:commandButton 中。该属性通过 id 指定应参与模型更新阶段的组件:
What you could do is disable the submit button from the time you initiate your ajax request until your ajax request completes. This will effectively prevent the user from pressing the submit button until it resolves:
One thing that is additionally helpful with this approach is if you display an "ajaxy" image to the end user while this is happening so that they intuitively understand that stuff is happening that will resolve soon. You can show this image in the onsubmit method as well and then hide it oncomplete.
EDIT:
The issue may just be that you need to add the process attribute to your a4j:commandButton. This attribute specifies the components by id that should participate in the model update phase:
我想我可以提供另一种解决方法。
我们应该在 js 级别有两个标志:
h:inputText
元素在模糊时阻止 ajax 请求:a4j:commandButton
如果requestBlocked
则发送请求false:如果
requestShouldBeSentAfterBlock
为 true,则a4j:support
发送请求:由于
oncomplete
块在重新渲染所有需要的元素后起作用,因此一切都会起作用按需要的顺序。i guess i can provide another work-around.
we should have two flags on js level:
h:inputText
element blocks the ajax request on blur:a4j:commandButton
sends the request ifrequestBlocked
is false:a4j:support
sends the request ifrequestShouldBeSentAfterBlock
is true:since
oncomplete
block works after all needed elements are re-rendered, things will work in the needed order.看起来原因在 getter/setter 中的某个地方。
例如,其中一种可能性:当没有
ajaxSingle=true
并且单击计算按钮时。所有值都设置为模型,因此setAge
和setDateOfBirth
都会被调用。并且可能会发生setDateOfBirth
在setAge
之前调用的情况。仔细观察
setAge
方法,它实际上将日期重置为年初,即使该日期可能已经有正确的年份。我建议首先简化逻辑。例如,为年份和生日设置单独的断开连接字段,并检查问题是否仍然可重现,以找到重现所需的最低条件。
同样从用户体验的角度来看,常见的做法是执行类似以下操作,因此一旦用户停止输入,事件就会触发:
It looks like a reason is somewhere in getters/setters.
For example one of the possibilities: when no
ajaxSingle=true
and the calculate button is clicked. All the values are set to the model, so bothsetAge
andsetDateOfBirth
are invoked. And it may happen so thatsetDateOfBirth
is invoked beforesetAge
.Looking closer at
setAge
method it in fact resets the date to the beginning of the year, even though the date could have had the right year already.I would recommend simplifying the logic first. For example have separate disconnected fields for year and birth day, and check if the issue is still reproducible to find minimal required conditions to reproduce.
Also from user experience standpoint, a common practice is to do something like the following, so the event fires once user stops typing: