使用 a4j:support 更新模型和视图,为下一个按钮/提交操作做好准备

发布于 2024-11-07 23:39:19 字数 5219 浏览 4 评论 0原文

问题

我们有一个基于 Swing 的企业应用程序前端,现在正在为其实现一个(目前更简单的)JSF/Seam/Richfaces 前端。

某些页面包含的字段在编辑时会导致其他字段发生更改。我们需要立即向用户显示此更改(即他们不必按按钮或任何东西)。

我已经使用 h:commandButton 成功实现了这一点,并将 onchange="submit()" 添加到导致其他字段更改的字段。这样,当他们编辑字段时就会发生表单提交,并且其他字段也会随之更新。

这在功能上工作得很好,但特别是当服务器负载很大时(这种情况经常发生),表单提交可能需要很长时间,并且我们的用户在此期间一直在继续编辑字段,然后当对 onchange="submit()" 请求被呈现。

为了解决这个问题,我希望实现以下目标:

  1. 在编辑字段时,如果需要,仅处理该字段,并且仅重新呈现它修改的字段 (这样用户在此期间所做的任何其他编辑都不会丢失)。
  2. 按下按钮后,所有字段都会正常处理并重新呈现。

(不稳定的)解决方案

好吧,我认为首先显示我的页面的一些内容可能是最简单的。请注意,这只是摘录,某些页面会有许多字段和许多按钮。

<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

此策略如下:

  1. ajaxSingle="true" 属性添加到 calculateButton
  2. 将带有 ajaxSingle="true" 属性的 a4j:support 标记添加到 firstName

第一步确保 calculateButton 不会覆盖 agedob 中的值,因为它不再处理它们。不幸的是,它有一个副作用,即它不再处理 firstName 。添加第二步是为了通过在按下 calculateButton 之前处理 firstName 来抵消这种副作用。

请记住,可能有 20 多个字段,例如 firstName。用户填写表单后可能会向服务器发出 20 多个请求!正如我之前提到的,这也是一种臃肿,可能会让其他开发人员感到困惑。

解决方法 2:使用进程列表

感谢 @DaveMaple 和 @MaxKatz 建议此策略,如下所示:

  1. ajaxSingle="true" 属性添加到 calculateButton
  2. 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:

  1. 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).
  2. 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:

  1. Add the ajaxSingle="true" attribute to calculateButton.
  2. Add the a4j:support tag with the ajaxSingle="true" attribute to firstName.

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:

  1. Add the ajaxSingle="true" attribute to calculateButton.
  2. Add the process="firstName" attribute to calculateButton.

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

待"谢繁草 2024-11-14 23:39:19

你的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.

深海不蓝 2024-11-14 23:39:19

最后,问题是:有没有办法确保模型和视图在发出calculateButton请求之前完全更新,这样就不会恢复它们?

您可以做的是从您发起 ajax 请求到 ajax 请求完成期间禁用提交按钮。这将有效地防止用户按下提交按钮,直到问题解决:

<a4j:support 
    event="onblur" 
    ajaxSingle="true" 
    onsubmit="jQuery('#mainForm\\:calculateButton').attr('disabled', 'disabled');" 
    oncomplete="jQuery('#mainForm\\:calculateButton').removeAttr('disabled');" />

此方法另外有帮助的一件事是,如果您在发生这种情况时向最终用户显示“ajaxy”图像,以便他们直观地了解正在发生的事情这很快就会解决。您也可以在 onsubmit 方法中显示此图像,然后在完成时隐藏它。

编辑:
问题可能只是您需要将 process 属性添加到 a4j:commandButton 中。该属性通过 id 指定应参与模型更新阶段的组件:

<a4j:commandButton 
    id="calculateButton" 
    value="Calculate" 
    action="#{illustrationManager.calculatePremium()}" 
    process="firstName"        
    reRender="mainForm" />

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?

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:

<a4j:support 
    event="onblur" 
    ajaxSingle="true" 
    onsubmit="jQuery('#mainForm\\:calculateButton').attr('disabled', 'disabled');" 
    oncomplete="jQuery('#mainForm\\:calculateButton').removeAttr('disabled');" />

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:

<a4j:commandButton 
    id="calculateButton" 
    value="Calculate" 
    action="#{illustrationManager.calculatePremium()}" 
    process="firstName"        
    reRender="mainForm" />
止于盛夏 2024-11-14 23:39:19

我想我可以提供另一种解决方法。

我们应该在 js 级别有两个标志:

var requestBlocked = false;
var requestShouldBeSentAfterBlock = false;

h:inputText 元素在模糊时阻止 ajax 请求:

<h:inputText id="dob" onblur="requestBlocked = true;" ...

a4j:commandButton 如果 requestBlocked 则发送请求false:

<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 块在重新渲染所有需要的元素后起作用,因此一切都会起作用按需要的顺序。

i guess i can provide another work-around.

we should have two flags on js level:

var requestBlocked = false;
var requestShouldBeSentAfterBlock = false;

h:inputText element blocks the ajax request on blur:

<h:inputText id="dob" onblur="requestBlocked = true;" ...

a4j:commandButton sends the request if requestBlocked is false:

<a4j:commandButton id="calculateButton"
    onkeyup="requestShouldBeSentAfterBlock = requestBlocked;
        return !requestBlocked;" ...

a4j:support sends the request if requestShouldBeSentAfterBlock is true:

<a4j:support event="onchange" 
    oncomplete="requestBlocked = false; 
        if (requestShouldBeSentAfterBlock) { 
            requestShouldBeSentAfterBlock = false;
            document.getElementById('calculateButton').click();
        }" ...

since oncomplete block works after all needed elements are re-rendered, things will work in the needed order.

半暖夏伤 2024-11-14 23:39:19

看起来原因在 getter/setter 中的某个地方。
例如,其中一种可能性:当没有 ajaxSingle=true 并且单击计算按钮时。所有值都设置为模型,因此 setAgesetDateOfBirth 都会被调用。并且可能会发生 setDateOfBirthsetAge 之前调用的情况。

仔细观察 setAge 方法,它实际上将日期重置为年初,即使该日期可能已经有正确的年份。

我建议首先简化逻辑。例如,为年份和生日设置单独的断开连接字段,并检查问题是否仍然可重现,以找到重现所需的最低条件。

同样从用户体验的角度来看,常见的做法是执行类似以下操作,因此一旦用户停止输入,事件就会触发:

<a4j:support event="onkeyup"
             ajaxSingle="true"
             ignoreDupResponses="true"
             requestDelay="300"
             reRender="..."/>

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 both setAge and setDateOfBirth are invoked. And it may happen so that setDateOfBirth is invoked before setAge.

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:

<a4j:support event="onkeyup"
             ajaxSingle="true"
             ignoreDupResponses="true"
             requestDelay="300"
             reRender="..."/>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文