I've been working with both the Apache MyFaces and the Oracle Mojarra teams on improving JSF 2 support in Metawidget.Metawidget is a more dynamic component than most, and exercises JSF 2 in a way few component libraries do. In particular, it stresses the relationship between dynamically modifying the component tree, partial state saving (new in JSF 2), and firing nested SystemEvents (new in JSF 2). As such, all 3 teams (MyFaces, Mojarra, Metawidget) have uncovered bugs in our implementations.
I'm delighted to say it looks like all these will be resolved in time for MyFaces 2.0.3 and Mojarra 2.2. For those interested in the 'correct' implementation of a dynamic JSF 2 component, as agreed by all teams, I've put together a little Acid Test that tests your JSF implementation for full compliance. In the process, it demonstrates the 'right' way to implement a dynamic JSF 2 component using SystemEvents. Which is:
public class UIAddComponent
extends UIComponentBase implements SystemEventListener {
public UIAddComponent() {
FacesContext context = FacesContext.getCurrentInstance();
UIViewRoot root = context.getViewRoot();
root.subscribeToViewEvent( PreRenderViewEvent.class, this );
}
public boolean isListenerForSource( Object source ) {
return ( source instanceof UIViewRoot );
}
public void processEvent( SystemEvent event )
throws AbortProcessingException {
if ( !FacesContext.getCurrentInstance().isPostback() ) {
// Safely manipulate component tree here
}
}
}
extends UIComponentBase implements SystemEventListener {
public UIAddComponent() {
FacesContext context = FacesContext.getCurrentInstance();
UIViewRoot root = context.getViewRoot();
root.subscribeToViewEvent( PreRenderViewEvent.class, this );
}
public boolean isListenerForSource( Object source ) {
return ( source instanceof UIViewRoot );
}
public void processEvent( SystemEvent event )
throws AbortProcessingException {
if ( !FacesContext.getCurrentInstance().isPostback() ) {
// Safely manipulate component tree here
}
}
}
My thanks to all teams for working so hard on this issue!
UPDATE: looks like this approach may be making it into the JSF spec: http://java.net/jira/browse/JAVASERVERFACES_SPEC_PUBLIC-1007

7 comments:
Hi Richard,
I am trying dynamically put components into the component tree. A figured using UIStableComponent from your Acid Test could be a good starting point. The problem is, I need to get an attribute of the component's tag: . I tried using getAttributes and this.getValueExpression but neither seems to find the attribute.
Lion,
Could you send some code to support@metawidget.org? Failing that, some pointers:
1. Are you using JSP (not Facelets)? In which case the 'tag' will be in a separate class altogether (one that extends javax.faces.webapp.UIComponentTag) so you need to look for your attribute there
2. If your component has a getter/setter for the attribute you are specifying, it will end up there.
Hope that helps,
Richard.
Hi Richard,
Using JSF2 (Mojarra 2.1), I've played around with your example and managed to bind a bean to the value attribute of an input field created in code. So far so good.
However, I run in to problems once I try to use this in a more complex scenario. I have a list of beans, each representing a complex property that should render itself into a form. I found that forEach is to early (for the pattern you suggest) and repeat is to late for binding to the values.
Could you elaborate on how to put the pattern into use in such a scenario?
Thanks,
Lior
I haven't tried running this inside a c:forEach, so that might be different. Could you put together some code and send it to support@metawidget.org?
Richard.
Hi,
Thank you for providing this Acid test example. I have been trying to dynamically create dataTables with JSF and I am finding it really difficult.
I modified the code for UIAddComponent to the following:
public void processEvent( SystemEvent event )
throws AbortProcessingException {
if ( !FacesContext.getCurrentInstance().isPostback() ) {
HtmlDataTable table = new HtmlDataTable();
JsfUtility.setValueExpression(table, "value", "#{company1.programmers}");
table.setVar("programmer");
getChildren().add(table);
HtmlColumn column = new HtmlColumn();
table.getChildren().add(column);
HtmlOutputText text = new HtmlOutputText();
JsfUtility.setValueExpression(text, "value", "#{programmer.firstName}");
column.getChildren().add(text);
column = new HtmlColumn();
table.getChildren().add(column);
text = new HtmlOutputText();
JsfUtility.setValueExpression(text, "value", "#{programmer.lastName}");
column.getChildren().add(text);
}
}
This works with MyFaces but doesn't with Mojarra 2.1. Mojarra recreates identical empty columns when the table is being populated and then throws an error when there is an id clash due to the identical columns.
Sincerely,
Rory.
Radha,
Yes, this is actually one of the Acid Tests (test 6 in EventTest.v8.zip).
The Mojarra guys are making steady progress and have recently fixed this particular part of the Acid Test. Please try your code with...
http://java.net/jira/secure/attachment/45087/1826.zip
And let them know whether it works at...
http://java.net/jira/browse/JAVASERVERFACES-1826
Also, vote for this issue! :)
Regards,
Richard.
Interesting find!
I additionally found that in JSF 1.2 or JSF 2.0 with partial state saving disabled, one is able to add any number of components dynamically to the tree in a backing bean's setter method for the binding property of a tag on a Facelet.
E.g. on a Facelet:
Bean:
public void setBind(UIComponent bind) {
UIViewRoot root = FacesContext.getCurrentInstance().getViewRoot();
// Components can be inserted here
root.getChildren().add(...);
}
This however does not work in JSF 2.0 (Mojarra 2.03 & 2.1.1) with partial state saving enabled. The component is added, and even renders, but does not survive a postback.
One would also expect that a (global) phase listener would be able to add components to the tree in BEFORE RENDER_RESPONSE. This however does not work. It seems logical that the tree would be fully restored AFTER RESTORE_VIEW, but for some reason it's actually only fully restored somewhere between BEFORE RENDER_RESPONSE and PreRenderViewEvent.
I created a global phase listener that just prints out the phase and the state of the view root; whether it's not null and if it has children. I then requested a simple page consisting of a JSF form with a button in it. The form was bound to a backing bean as shown above. Additionally, the global phase listener registered itself for the PreRenderViewEvent.
This is the result on Mojarra 2.1.1:
18:56:22,910 INFO [javax.faces.event.PhaseListener] before RESTORE_VIEW 1
18:56:22,910 INFO [javax.faces.event.PhaseListener] Viewroot null
18:56:22,911 INFO [javax.faces.event.PhaseListener] after RESTORE_VIEW 1
18:56:22,911 INFO [javax.faces.event.PhaseListener] Viewroot not null
18:56:22,911 INFO [javax.faces.event.PhaseListener] Viewroot has no children
18:56:22,911 INFO [javax.faces.event.PhaseListener] before RENDER_RESPONSE 6
18:56:22,911 INFO [javax.faces.event.PhaseListener] Viewroot not null
18:56:22,911 INFO [javax.faces.event.PhaseListener] Viewroot has no children
18:56:22,912 INFO [com.test.BackingBean] setBind for backing bean called.
18:56:22,912 INFO [javax.faces.event.PhaseListener] Viewroot not null
18:56:22,912 INFO [javax.faces.event.PhaseListener] Viewroot has children
18:56:22,912 INFO [javax.faces.event.PhaseListener] PreRenderViewEvent fired.
18:56:22,912 INFO [javax.faces.event.PhaseListener] Viewroot not null
18:56:22,913 INFO [javax.faces.event.PhaseListener] Viewroot has children
18:56:22,914 INFO [javax.faces.event.PhaseListener] after RENDER_RESPONSE 6
18:56:22,914 INFO [javax.faces.event.PhaseListener] Viewroot not null
18:56:22,914 INFO [javax.faces.event.PhaseListener] Viewroot has children
As you mentioned, components added in PreRenderViewEvent render and survive a postback, but in the case of Mojarra only in 2.1.1. In the often used Mojarra 2.0.3 this also does not work, but as mentioned: when partial state saving is disabled components can be added in the setBind method.
FInally, components added AFTER RENDER_RESPONSE do not survive a postback.
Post a Comment