Friday November 30, 2007
NetBeans Visual Web - JSF, "Pretty" URLs and Multiple Instances of the Same Page
Since I introduced "Pretty URLs" in
the
NetBeans Plugin Portal, there have been some strange things
happening like issues
119249,
122600,
and
122340.
Once again I've been schooled on the JSF life cycle.
So here's what's happening.
Problem
When a button or link is clicked on a page and an action is associated
on the pages backing bean, a post back occurs. For the
purposes of this discussion, a post back basically means a request is
sent from the displayed web page to the server, the page backing bean
action method is executed, a null is returned from the method telling
the JSF framework not to navigate anywhere, and the page is redisplayed
using the JSF Standard Processing Lifecycle. If you look at
the how I used the JSF life cycle in my earlier blog titled,
"NetBeans
Visual Web Pack - Real World Apps Tip #4 - "Pretty" URLs", I
took advantage of the
Visual
Web application model "init" method. In the details
of implementing "Pretty" URLs, the key piece of information for
rebuilding the state of the page in the "init" would logically be put
on the SessionBean. For example, for the Plugin Detail page I
need the "currentPlugin". So when I navigate to the Plugin
Detail page I set the "currentPlugin" attribute on the SessionBean and
use that for all the details. So now what happens when
someone right-clicks on my "Pretty" URL and chooses to open a different
Plugin in another tab? The Plugin Portal stores the new
Plugin as the "currentPlugin" on the SessionBean. Now on the
first plugin, I do some action like click the download button.
Now to the important part! Remember that a PageBean
gets instantiated EVERY time a page is displayed. This means
the PageBean for the first Plugin is instantiated and gets the
"currentPlugin" from the SessionBean. Things are now bad
because the second Plugin in the other tab set "currentPlugin".
So you have results like described in the issues above.
Solution
The solution for this seemed easy at first, save each page instance
state per Plugin. This turned out to be quite difficult to
implement. Again I realized that the biggest difficulty when
using JSF is understanding the Standard Processing Lifecycle.
I somehow needed to save off state from a page with
uniqueness and pull that state back. For example, if a detail
page was showing for a particular Plugin and the user opens another
instance of the page with a different plugin, we need to have each page
somehow maintain which plugin is which for a post back.
Remember we can't use the SessionBean for "currentPlugin"
because each instance of the page will overwrite this attribute.
We can't use cookies because these are browser instance wide
and not page specific. The black magic is based on a nice
class in the JSF framework called "UIViewRoot". The
"UIViewRoot" is the root of the component tree for all the components
on the page. And here's the best part, "UIViewRoot" is saved
across post back calls. This means on a post back, you can
expect values set in components to be there when the page is
rerendered. This implementation does things like keeping form
values across post backs to keep users form committing suicide after
they've filled out a form for 3 hours only to hit the "submit" button
and find out they missed a required field and now all the fields are
blank. The Visual Web application model has the "init" method
that happens during the "Restore View" phase.
The problem is that the
UIViewRoot values don't get stuffed back into the components on the
backing bean until "Update Model Values". This means you
can't do something like this,
this.getTxtPluginID().getText();
and expect to get the value you stuffed in there before a button was
clicked. So the trick is to pull the value out of the
"UIViewRoot". Here's the code that shows how I did this on
the Plugin Detail page.
/**
* Process a GET type request.
*/
HttpServletRequest request = (HttpServletRequest)FacesContext.getCurrentInstance().getExternalContext().getRequest();
String [] pluginids = request.getParameterValues("pluginid");
if(null != pluginids && pluginids.length == 1) {
try {
Long pluginid = new Long(Long.parseLong(pluginids[0]));
currentPlugin = getApplicationBean1().getPluginSystem().getPlugin(pluginid);
if(null != currentPlugin) {
getSessionBean1().setCurrentPlugin(currentPlugin);
getRequestBean1().setCurrentPlugin(currentPlugin);
this.getTxtPluginID().setText(currentPlugin.getPluginid());
}
} catch(NumberFormatException nfe) {}
} else {
if(null == this.getCurrentPlugin()) {
/**
* Process a post back.
*/
FacesContext fc = FacesContext.getCurrentInstance();
UIViewRoot viewRoot = fc.getViewRoot();
if(null != viewRoot) {
StaticText pluginid_text =
(StaticText) viewRoot.findComponent("form1:center_container:page_border:fixed_contentarea:fixed_contextbox:topPanel:txtPluginID");
if(null != pluginid_text) {
Long pluginid = (Long)pluginid_text.getText();
if(null != pluginid) {
currentPlugin = getApplicationBean1().getPluginSystem().getPlugin(pluginid);
}
}
}
}
}
So you can see that for a "GET" style call with the "Pretty" URL we do
what did before except we stuff the part of the state we want for the
post back into a "Static Text" component. In the case of the
post back, we get a hidden field from the "UIViewRoot" and get its
value to use. So the one last piece is to create a "Static
Text" component somewhere on the page and make it invisible.
As you can see from the code you have to fully qualify the
name of the "Static Text" component. In my case it's
"txtPluginID".
Well I'm hoping another brick doesn't fall out the other side again.

If anyone has a better way to pull this off, please, please
post a comment.
Posted by david
( Nov 30 2007, 12:53:36 AM MST )
Permalink

|

|
Trackback URL: http://blogs.sun.com/david/entry/netbeans_visual_web_jsf_pretty