Last week I learned a tough lesson in AJAX, JSF and client-side component state. This one had me fooled for the better part of a day, in part due to the complexity of debugging multiple related AJAX requests, intermittent symptoms, and the opaque nature of JSF component state. Here's what happened:
The page that was problematic has multiple tabs (rich:tab), some combo boxes (h:selectOneMenu), and at least one suggest control (rich:suggestionbox). The tabs are server-side AJAX tabs. The suggest control also uses AJAX.
To reproduce the problem, the user takes the following steps:
- Use a suggest control to select a value on the first tab of a
- Switch tabs to a different part of the form
- Press a button on the form in the 2nd tab
At this point the application complains with validation errors on the first tab. The experience is very confusing to the user: after switching tabs successfully with no errors, pressing a button on the second tab causes validation errors on the first tab. This should never happen in the application: validation is designed to occur only for components that are submitted. When the button is pressed on the second tab, validation should not occur for components in the first tab. So what's going on?
Here's what I expected to happen:
- The AJAX suggest control sends requests and renders itself
- The tab sends requests and renders its contents
- The button submits the form to make a request
Here's what really happened, depending on timing of the requests:
- The AJAX suggest control sends a request
- The tab sends a request
- The browser receives the response from the tab click and re-renders the tab and its form controls
- The browser receives the response from the suggest control
After receiving every response the DOM is updated to contain the new JSF component state (remember: we're using client-side component state, so all JSF component state is stored in a hidden form parameter in the browser).
Due to out-of-order responses, the component state was being set to that of the first tab by the suggest control -- thus the application thinks that the current tab is the first, not the second as we would expect. Pressing the button on the second tab results in the application interpreting the submitted form as submitting values for components on the first tab. This is problematic for HTML <select> controls, since HTTP parameters are not submitted for <select> controls where no value is selected.
So how do we fix this problem? Have all AJAX controls use the same request queue. This is achieved by setting the attribute
eventsQueue="myApplicationEventQueue" on all of the richfaces AJAX controls. By doing so we end up with the following sequence:
Lesson learned: AJAX requests update JSF component state on the client, and thus must all complete in sequence. Otherwise component state on the client can get out of sync with what we expect it to be.