JSF in SF

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:



<html>
<head>
</head>
<body>
<form name="foo">
<div id="valCtr">
<input name="val" type="hidden" svalue="1">
<script>
document.forms.foo.val.value=1;
</script>
</div>

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


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);
}

</script>

<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">
</script>

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">
document.forms.foo.val.value=1;
</script>


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.

Wednesday, May 23, 2007

A year already? So long incubator!

How time flies... A year ago, we checked ADF Faces into the Apache incubator. Now, we're out of the incubator, we're named Trinidad, and we're officially part of the Apache MyFaces project. You can visit our site, and download nightly builds .

A lot has happened in this past year - a few highlights:
  • A bunch of new committers were added from inside Oracle and, most importantly, from outside Oracle
  • All the ins-and-outs of running an Apache project were ably handled by Matthias Wessendorf
  • Skinning functionality has gotten a lot better, mostly courtesy of Jeanne Waldman
  • Portlet support from Scott O'Bryan
  • Client-side validation now looks much better (no more JS alerts), from Danny Robinson
  • Lots and lots of bugs (300+) were put to ground
  • New components - a spinbox and an outputDocument
  • JSF 1.2 support was implemented (on a branch); the MyFaces implementation of the 1.2 JSF API uses a Trinidad plugin to generate components and tags.
  • And, I got engaged! (The future Mrs. even lets me get away with working on Trinidad at home.)

It all took awhile, and a lot of work, but we're all glad to have reached this point.

Sunday, May 07, 2006

ADF Faces checked in to open source !


Transmitting file data .........................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
...........................................
Committed revision 404824.


... and it's done. ADF Faces has been checked into an Apache incubator at http://svn.apache.org/repos/asf/incubator/adffaces/trunk/. Now the real work begins!

Thursday, March 30, 2006

Facelets 1.1.2 is out!

As Jacob Hookom just announced, Facelets 1.1.2 is now available. This is a major milestone for Facelets: the big issues have been nailed, templating's solidified, and excellent features like <ui:repeat> are in there too. Give it a whirl!

(And, for this blog, that "build before restore" feature that's part of saving JSF state saving has its first public appearance here too.)

Wednesday, March 22, 2006

Usings Sets with UIData

One of the more personally surprising usability complaints I've heard was that the JSF UIData component does not support Sets. I hadn't anticipated that one. The underlying reason is that UIData is built around indexed data. For example, you can ask it to show rows 10,000 through 10,099. Such an operation would be nightmarishly expensive in a Set:



Iterator iter = collection.iterator();
for (int i = 0; i < 10000; i++) { iter.next(); }
// Hey, now we can start reading our data!



But, hey, we still got it wrong: we should have supported it. Yeah, it'd be slow - O(N) where N is the size of the set, not the amount of data actually displayed at one time - but the ease of use argument is compelling.

That said, there's nothing stopping you from using Sets with UIData right now... if you use the following class:


import java.util.AbstractMap;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.Set;

public class ListFromCollection
{
public ListFromCollection()
{
_map = new MakeList();
_size = _DEFAULT_SIZE;
}

public Map<Collection, List> getList()
{
return _map;
}

public int getSize()
{
return _size;
}

public void setSize(int size)
{
_size = size;
}

private class MakeList extends AbstractMap<Collection, List>
{
public List get(Object o)
{
if (!(o instanceof Collection))
return null;

// Just send RandomAccess lists out; wrap any other Collection
// into a List
if ((o instanceof List) &&
(o instanceof RandomAccess))
return (List) o;

Collection c = (Collection) o;
if (c.isEmpty())
return Collections.EMPTY_LIST;

return new ListImpl(c, getSize());
}

public Set<Map.Entry<Collection, List>> entrySet()
{
// Not worth implementing at the moment; this Map is only
// accessed from
return Collections.emptySet();
}
}

static private class ListImpl extends AbstractList
{
public ListImpl(Collection c, int size)
{
_c = c;
_cSize = c.size();
if (size == 0)
_bufferSize = _cSize;
else
_bufferSize = Math.min(size, _cSize);

_buffer = new ArrayList(_bufferSize);
_offset = -1;
}

public int size()
{
return _cSize;
}

public Object get(int index)
{
if ((index < 0) || (index >= _cSize))
throw new IndexOutOfBoundsException();

int offset = (index / _bufferSize) * _bufferSize;
if (offset != _offset)
{
_loadBuffer(offset);
_offset = offset;
}

return _buffer.get(index - _offset);
}

private void _loadBuffer(int offset)
{
Iterator iter = _c.iterator();
int i = 0;

while (i < offset)
{
assert iter.hasNext();
iter.next();
i++;
}

_buffer.clear();

int count = 0;
while ((count < _bufferSize) && (i < _cSize))
{
assert iter.hasNext();
_buffer.add(iter.next());
i++;
count++;
}
}

private final Collection _c;
private final int _bufferSize;
private final int _cSize;
private int _offset;
private ArrayList _buffer;
}

private Map<Collection, List> _map;
private int _size;

static private int _DEFAULT_SIZE = 50;
}


So, what's this all mean? Well, say you want to write:


<h:dataTable value="#{mySet}">


... but that doesn't work. Just add one managed-bean entry to your faces-config.xml:


<managed-bean>
<managed-bean-name>makeList</managed-bean-name>
<managed-bean-class>
yourPackageHere.ListFromCollection
</managed-bean-class>
<managed-bean-scope>
request
</managed-bean-scope>
</managed-bean>


... and now, you can use sets on dataTable via:


<h:dataTable value="#{makeList.list[mySet]}">

Thursday, March 16, 2006

Fixing JSF state saving: Save vs. Restore

(A bit of esoterica, but probably of interest here to those deeply focused on JSF performance...)

An update: I got a chance to take some timings. As expected, saving state is now much faster: 85-90% faster, roughly equivalent to the improvements in the size of the saved state. But I was very surprised to discover that restoring the view isn't any faster, perhaps even marginally slower - until I realized that was the only possible result.

Restore View has to create a full tree, starting from zero. What could possibly be faster than creating that tree from an object tree that specifies every attribute that needs to be set, every class name that needs to be instantiated? Put another way, the old restoreVIew() approach, for all of its memory inefficiencies, is essentially an equivalent workload to building the tree in the first place, and might be even faster. This new approach starts with the overhead of building the tree from scratch, plus the overhead of setting some additional state. (That additional overhead is fairly negligible, but the point remains: fixing JSF state saving doesn't make Restore View fast.)

A slight clarification, BTW: I'm not using client-side state saving a la the RI or MyFaces, which serializes the entire tree to the client. Instead, we use ADF Faces's tokenized client-side state saving - run saveSerializedView, but then stash the result on the session, and only send a token to the client. If we were using fully serialized trees, then you would get a performance boost even in Restore View, because you'd only have to unserialize a small block, instead of the whole thing.

So, does that mean that restoreView() is necessarily slow? Well, no. There are approaches to optimizing restoreView(). The most direct is optimizing building the component tree - which will help both with initial component tree creation and with restoreView(). How to do that in another post. (Hint: FacesBean and Facelets are a huge help here, too!)

Wednesday, March 15, 2006

Fixing JSF state saving: a progress report

I've implemented the first version of an improved JSF state saving architecture, essentially what I talked about in my last post.

The very latest and greatest Facelets source code has a facelets.BUILD_BEFORE_RESTORE initialization parameter. When turned on, it changes ViewHandler.restoreView() to build the view before asking the StateManager for help.

And the latest ADF Faces code adds a new markInitialState() method to our components - which calls through to FacesBean.markInitialState(). When our MyFaces svn repository is set up, you'll be able to see this code too...

And the results? Well, one test case with a fairly large page dropped from nearly 10K of client-side state to only 600 bytes. That's more than a 90% reduction in saved state! (Presumably, CPU usage is way down, but I haven't measured that yet.)

There's still potential improvements: I could get that 600 bytes down further by overriding the state saving in UIViewRoot. Also, Jacob's suggestion to use a flat Map instead of a hierarchy would also help - currently, if one component deep in the hierarchy needs to store state, we build up a large hierarchy of mostly null Object arrays. This isn't especially expensive, but it is avoidable.

A footnote: Mike Youngstrom cogently noted in a comment to my last post that a delta approach to state saving would let us make UIData both simpler and more generic - instead of handcoding a specific set of properties to save on certain types of child components, just run state saving. Absolutely, Mike! I haven't prototyped this on our UIXTable component (which doesn't extend UIData, FWIW), but that's a great thing to try.