Tuesday, January 13, 2009

tr:switcher has some lifecycle caveats

The Apache MyFaces Trinidad has some optimizations in the component that can easily cause application exceptions.

The switcher only decodes, validates, updates and renders the facet that is currently show. For example:

<tr:switcher defaultFacet="#{bean.facet}">
 <f:facet name="one" />
 <f:facet name="two" />
</tr:switcher>

If bean.facet evaluates to "one", then only the facet with that name will be decoded, validated, updated and rendered. This means, that if the bean value is changed, another component will be acted upon. Take this example:

<tr:selectOneChoce id="soc1" value="#{bean.facet}" autoSubmit="true">
 <f:selectItem itemLabel="One" itemValue="one" />
 <f:selectItem itemLabel="Two" itemValue="two" />
</tr:selectOneChoce>
<tr:switcher id="sw1" defaultFacet="#{bean.facet}">
 <f:facet name="one">
   <tr:inputText id="it1" value="#{bean.one}" />
 </f:facet> 
 <f:facet name="two">
   <tr:inputText id="it2" value="#{bean.two}" />
 </f:facet> 
</tr:switcher>

What will happen is this:

  1. APPLY_REQUEST_VALUES
    1. soc1 is decoded (submitted value is set)
    2. sw1 is decoded
    3. it1 is decoded (submitted value is set)
  2. PROCESS_VALIDATIONS
    1. soc1 is validated (value is converted and set as a local value)
    2. sw1 is validated
    3. it1 is validated (value is converted and set as a local value)
  3. UPDATE_MODEL_VALUES
    1. soc1 is updated (backing bean #{bean.facet} is set to new value)
    2. sw1 is updated
    3. it2 is update (no submitted or local value)

As you can see it1 is decoded and validated, but not updated. it2 is updated but not decoded or validated.

Now, lets consider the Trinidad tree (UIXTree component). Inside the decode of the tree, the code ensures that the component has been initialized using its __init() method. This method ensures that the tree's disclosed and selected row key sets are not null. Since the JSF phases dictate that the decode method will be called before the updateModel method, the init method is not run for the other phases. Now, if the switcher is used as it is above and there is a tree in one of the facets, this means that the tree may not have ever initialized its disclosed row keys.

The 'fix'

To ensure that a component's lifecycle is fully run, that means that the switcher's facet cannot be changed between the decode and the end of the update model phase. This basically means that the value should only be changed during the INVOKE_APPLICATION phase. So here is an example of that:

<tr:selectOneChoce id="soc1" valueChangeListener="#{bean.switch}" autoSubmit="true">
 <f:selectItem itemLabel="One" itemValue="one" />
 <f:selectItem itemLabel="Two" itemValue="two" />
</tr:selectOneChoce>
<tr:switcher id="sw1" defaultFacet="#{bean.facet}" binding="#{bean.switcher}">
 <f:facet name="one">
   <tr:inputText id="it1" value="#{bean.one}" />
 </f:facet> 
 <f:facet name="two">
   <tr:inputText id="it2" value="#{bean.two}" />
 </f:facet> 
</tr:switcher>
public class Bean
{
 private String facet;
 private UIXSwitcher switcher;

 public Bean()
 {
   this.facet = this.selectFacet = 'one';
 }

 public UIXSwitcher getSwitcher() { return this.switcher; }
 public void setSwitcher(UIXSwitcher switcher) { this.switcher = switcher; }

 public String getFacet() { return this.facet; }

 public void switch(ValueChangeEvent event)
 {
   if (event.getPhaseId() != PhaseId.INVOKE_APPLICATION)
   {
     event.setPhaseId(PhaseId.INVOKE_APPLICATION);
     event.queue();
     return;
   }
   this.switcher.setFacetName((String)event.getNewValue());
 }
}

Now this is a bit of a hack, but this ensures that the callback to set the switcher only occurs during the correct phase.

Change UIXSwitcher?

A very good argument could be made that UIXSwitcher should not prevent the decoding, validating and updating of all of its children. This would be more acceptible from a JSF perspective. The problem with this is that it would drastically impact performance of users that are expecting this behavior.

Wrap Up

The take away, is that any component that affects the lifecycle methods of other components must be used with care. A component should always decode, validate and update in that order, and should never have its lifecycle changed. Hopefully this post will help avoid problems that are not that easy to debug.

2 comments:

Richard said...

Thanks for the post! I think it explains why I'm having to force a page refresh of the view root instead of having the contents of my switcher get updated by PPR.
I notices some typos in your example. I think there should be a binding attribute in the switcher so that the component is bound to "#{bean.switcher}". Also both of the facets in the switcher are named "one". I think the second one should be "two".

Andrew said...

Thanks for comment and pointing out the typos, I fixed them.

BTW, you don't need to PPR the entire page, just PPR the parent component of the switcher.