Tuesday, July 31, 2007

AJAX and the Refresh Button

JSF relies heavily on <input type="hidden" name="javax.faces.ViewState"> for its lifecycle. This hidden field carries all UI state for the page. Whether that's client-side state (with the entire page Base64-encoded) or server-side state (with a simple token), it's important that the right field be delivered with any JSF postback for the page to function correctly.

As a result, AJAX implementations in JSF typically need not only to submit this value to the server when posting an AJAX request, but also to update it as necessary when a request completes. While looking at an ADF Rich Client bug recently, I was rudely reminded that the Refresh/Reload button doesn't always behave as you might imagine, and thought it was worth delving into the details. (I'll be talking about JSF, but the behavior is generic to DHTML and applies outside of JSF, and the code samples are just raw HTML.)

Take the following page:

<form name="foo">
<div id="valCtr">
<input name="val" type="hidden" svalue="1">

<a href="#" onclick="forms.foo.val.value =
parseInt(forms.foo.val.value) + 1; return false;">

Now, in your favorite browser, try the following:
  • Click Display (you'll see 1)
  • Click Increment a couple of times
  • Click Display again (you'll see 3)
  • Click or select Refresh/Reload (but not Shift-Refresh)
  • And Display once more. You'll still see 3 (unless you're using Safari)
  • Now, Shift-Refresh, and Display. Now we're back at 1.
What have we seen? Refresh has re-queried the HTML for the page, but instead of resetting the value of our hidden input field back to 1, it's stored the updated value of 3! This isn't a bug - so says this Bugzilla bug (and all 61 duplicates!) Microsoft would agree with Mozilla here. Of the big 3, only Safari doesn't overwrite form fields on reload. (The caching behavior is in fact very handy for Back/Forward, and is exploited by the Really Simple History framework.) Shift-refresh fully reloads the page, and drops the form element cache.

This can lead to big problems in a JSF application. Take the following scenario:
  • A page initially renders with state token 1 in a hidden field
  • An AJAX request updates the state token to 2
  • The user hits Reload, and the new HTML contains state token 3
  • But the browser ignores it, and overwrites it with state token 2!
Now we've got a page in state "3", but a token claiming it's really in state "2". This is bad. As always, let's see what alternatives we've got, and whether they suffer from the same problem.

First, how about creating the DOM on the fly?

<script type="text/javascript">
function incrementViaDOM()
var value = parseInt(document.forms.foo.val.value) + 1;
var newField = document.createElement("input");
newField.name = "val";
newField.type = "hidden";
newField.value = "" + value;
var oldField = document.forms.foo.val;
var parent = oldField.parentNode;
parent.replaceChild(newField, oldField);


<a href="#" onclick="incrementViaDOM(); return false;">
Increment with replaceChild</a>

In Firefox, this still doesn't help. The hidden field value is still cached. (And this example doesn't work at all in IE... see this entry for why and how to fix it.)

OK, how about innerHTML?

<script type="text/javascript">

var value = parseInt(document.forms.foo.val.value) + 1;
var valCtr = document.getElementById("valCtr");
valCtr.innerHTML = "<input name=\"val\" " +
"type=\"hidden\" value=\"" + value + "\">";

<a href="#" onclick="increment(); return false;">
Increment with innerHTML</a>

Now this works... changes made by innerHTML are not remembered by Firefox or Internet Explorer (or Safari). So, if you don't want a hidden field cached, update with innerHTML and browsers won't hassle you.

Alternatively, you could look at tackling this issue during the refresh, by forcibly resetting the value from Javascript:

<input name="val" type="hidden">
<script type="text/javascript">

This works in Firefox, but does not in IE. Whatever code overwrites the value of the hidden field runs after this inline script, but before the page's onload handler. So, if you want to tackle this problem while refreshing, you'll have to do it in onload.

To (finally) come back to JSF, there's a better way to solve this problem, at least for the state token. Use a StateManager that automatically doesn't generate new tokens for AJAX requests, but instead reuses the old token. New tokens are important when you're rendering a new page, but are a waste of space when you're just working on a single page. And, as a nice side-effect, this makes this issue moot. (MyFaces Trinidad 1.0.2 will include this token-reuse optimization, though it's always used innerHTML for updating the state token, so it hasn't been hit by this bug.)

So, to summarize, if you have a programatically-modified, hidden input field that needs to be Reload-proof, two techniques look good:
  • Use innerHTML to update the field
  • Use onload to set the hidden input field value

If you've got any other tricks, I'd be happy to know.


Post a Comment

<< Home