6

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;
    }
}
Gyan aka Gary Buyn
  • 12,242
  • 2
  • 23
  • 26

4 Answers4

0

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.

tt_emrah
  • 1,043
  • 1
  • 8
  • 19
0

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="..."/>
Andrey
  • 6,526
  • 3
  • 39
  • 58
0

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.

Max Katz
  • 1,582
  • 1
  • 9
  • 5
  • The beans (Seam components) are all session scoped. – Gyan aka Gary Buyn May 17 '11 at 20:45
  • If you use a queue, then the first request (onchange) should finish before the 2nd one (button) is sent. So, your model is updated before the 2nd request is fired. When you say the values are reverted when the button is clicked - what does it mean? – Max Katz May 17 '11 at 21:10
  • Yeah that's what I thought. When I press the button after entering a new age, two things happen in this order: 1. The DOB field is updated with the new value, implying that the model has been updated. 2. There is a pause (as the calculations take a while) at the end of which the DOB and age revert to their original values. I will add their getters and setters to my question since they are a little complicated and maybe they are the source of my woes? – Gyan aka Gary Buyn May 17 '11 at 21:26
  • Components get their values from the server so something is changing their values there (for DOB, age). The button has ajaxSingle=true, which means the inputs will not be processed. Let's say I enter 25 for age. What happens on the page? – Max Katz May 18 '11 at 04:15
  • Thanks for your input Max. When the button has `ajaxSingle=true` it works for me (as long as all my input controls also have `a4j:support` with `ajaxSingle=true`). That was my workaround. My question was whether I could do it without `ajaxSingle=true` on my button and without `a4j:support` with `ajaxSingle=true` for all my input controls that do not cause other field values to change. – Gyan aka Gary Buyn May 18 '11 at 08:15
  • I have added to my question recently. Maybe it makes more sense now? – Gyan aka Gary Buyn May 18 '11 at 08:16
  • I guess I'm still not clear why firstName is reverted back (from your workaround). If you enter 25, to what value does it revert? But, if you need to process firstName component with the button, you can add process="firstName" to the button. – Max Katz May 18 '11 at 16:37
  • `firstName` reverts to its previous value when I add `ajaxSingle="true"` to the button but **not** to the field `firstName` since `firstName` is never processed. This is behavior that I expect and understand. You are right that if I added `process="firstName"` to the button it would also work. I understand why the workaround works but I don't think it is a very good solution. – Gyan aka Gary Buyn May 18 '11 at 21:02
  • Whatever pattern I end up using here may end up being used in our whole application. In some pages there may be 20 fields like `firstName` and I would prefer not to have to add them all to the `process` list. I would also prefer to not have to add `a4j:support` to all those fields unless there is a real reason for it, like causing an update to another value in the model. This is mainly because it would cause an unnecessary amount of requests and network traffic. These steps could also be easily forgotten when adding a field to a page. – Gyan aka Gary Buyn May 18 '11 at 21:03
  • @Max Katz is "process" limited to only the a4j form that the data is in? – Kevin Sep 09 '11 at 19:07
  • @Kevin: I think you can point to components outside the current form.. but just try it and see if it works. – Max Katz Sep 12 '11 at 17:38
  • @Max Katz I just tried, looks like it doesn't work. Rather, I just threw everything into a large a4j:form and seperated them by regions. That way process works for me. Thanks though – Kevin Sep 12 '11 at 18:44
0

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" />
Dave Maple
  • 8,102
  • 4
  • 45
  • 64
  • When I use the `a4j:support` tag in `firstName`, I don't have the problem, and the AJAX request is not submitted until I press `calculateButton`. The thing is that I don't believe I should have to add it to `firstName` at all. I do use an 'ajaxy' image. RichFaces includes [a tag that does it automatically](http://livedemo.exadel.com/richfaces-demo/richfaces/status.jsf). – Gyan aka Gary Buyn May 17 '11 at 21:00
  • Oh. OK. I misunderstood. You're saying that if you remove the ajax your action method #{illustrationManager.calculatePremium()} is being executed before your model has updated? for example #{life.firstName}? – Dave Maple May 17 '11 at 21:32
  • Exactly, since `calculateButton` has `ajaxSingle="true"`, it does not process the value for `firstName`. I added `a4j:support` to `firstName` so that it gets processed before `calculateButton`. All of this is just a workaround though. – Gyan aka Gary Buyn May 17 '11 at 21:42
  • did you try adding process="firstName"? That attribute defines the list of ids that will be processed in the ajax request. – Dave Maple May 17 '11 at 21:51
  • Thanks, I hadn't thought of that. That would be a less chatty workaround. Still though, it's another thing I don't believe I should have to do. I can see it being a source of bugs (as is my workaround). – Gyan aka Gary Buyn May 17 '11 at 22:37
  • if you just want a standard component that will submit all the elements in your form you can switch to h:commandButton which will do just that, but in order to optimize the performance of partial page requests (ajax) it's typical to specify the components that you want to execute/process on the server. In jsf 2.0 you can specify execute="@form" which is handy when you want ajax but want the whole form processed. – Dave Maple May 17 '11 at 22:49
  • I guess I don't understand. Earlier you said that the issue you had was your model not being updated. If you add process="firstName" and add the additional components you want processed then your model will be updated, thus solving your problem. You can then remove the a4j:support from the individual components like you wanted to. I think that is what you are trying to accomplish and is not really a "workaround". – Dave Maple May 18 '11 at 00:07
  • Sorry, you're right, I went straight into explaining the problem without explaining why I need it to be solved. I will add this to my question now. – Gyan aka Gary Buyn May 18 '11 at 00:39