Wednesday, January 4, 2012

Component scope

It is often confusing to users what it means by having a JSF component be "in scope," especially in JSF 2. JSF is processed by a set of phases, each one usually iterates the component tree, acting upon each component in turn. Some components, like the h:dataTable, perform stamping.

Note, for more information on stamping, please see my blog on tables.

When a component is being processed, the EL context may be altered. For example, the data table injects "var" and "varStatus" variables into the request scope. This also means that when the table is not being processed, these variables are either not available, or point to another value.

This is what I mean by a component being "in scope," that it is currently being processed by the JSF lifecycle or JSF APIs in a manner congruent with the JSF specification for working with components.

How a component enters scope

A component can enter scope one of three ways:

  1. The JSF lifecycle is being run. This will be the result of calls from methods on the component:
    • processDecodes
    • processValidators
    • processUpdates
    • processSaveState
    • processRestoreState
    • broadcast
  2. During an invokeOnComponent callback
  3. During a visitTree callback

At any other time, it may not be valid to be interacting with the component. Take for example, using Oracle ADF components, an input text component inside of an iterator:

<af:iterator var="item" value="#{bean.items}">
  <af:inputText value="#{item.value}" binding="#{bean.inputText}" />
</af:iterator>

Now consider this Java code for the bean:

private RichInputText _inputText;

public RichInputText getInputText()
{
  return _inputText;
}

public void setInputText(RichInputText text)
{
  _inputText = text;
}

public Object getValue()
{
  return _inputText == null ? null : _inputText.getValue();
}

Should code call the bean's getValue() method, what is the outcome? Well that is indeed the problem. Before the JSF view is built, the value will be null as the component has not been bound yet. Between JSF phases, it will also be null because #{item} will not be present in the EL context.

As this example shows, the attributes and behavior of the component in question change based on what component is currently being operated on. What makes this even more of an issue, is component frameworks that setup and tear down contexts. For example, Oracle ADF faces has a base component class called a oracle.adf.view.rich.component.fragment.ContextSwitchingComponent. When a component is processing its JSF lifecycle, or one of its children is being called back via a visitTree or invokeOnComponent call, the setupContext method has been run. If one of these components is accessed through code, and the context has not been set up yet, unreliable results may occur, or even exceptions may be thrown. One use case is of the ADF page template component. By using context switching, this component alters the EL context so that facets in the page with the <af:pageTemplate /> tag are executed in the EL context they were defined in, and the components inside the page template definition are executed within the context of the page template definition file.

Note that JSF 2 has a compound component like the ADF page template, but I am not sure how it handles EL context resultion.

It is therefore, never a good idea to call methods or evaluate attributes on JSF components unless the caller is sure that the component being accessed is currently in scope. Here is another example:

<af:iterator var="item" value="#{bean.items}">
  <af:pageTemplate viewId="template.jspx" item="#{item}">
    <f:facet name="center">
      <af:outputText id="facetText" value="#{item.itemValue}" />
    </f:facet>
  </f:pageTemplate>
</af:iterator>

Page definition:

...
<af:pageTemplateDef var="attrs">
  <af:xmlContent>
    <component xmlns="http://xmlns.oracle.com/adf/faces/rich/component">
      <description>This component lays out an entire page.</description>
      <facet>
        <description>The center content.</description>
        <facet-name>center</facet-name>
      </facet>
      <attribute>
        <description>the item.</description>
        <attribute-name>item</attribute-name>
        <attribute-class>com.mycompany.MyItem</attribute-class>
      </attribute>
    </component>
  </af:xmlContent>
  <af:iterator var="item" value="#{item.innerItems}">
    <af:facetRef facetName="center"/>
    <af:outputText id="templateText" value="#{item.itemValue}" />
  </af:iterator>
</af:pageTemplateDef>

This is a very simplified example, but it illustrates the question, what does #{item.itemValue}" evaluate to? In the facet, this should be the item from the bean's items collection, as shown with the output text with id "facetText". Inside the page template, the value should be the item from the inner items collection of the item passed into the page template, show by the output text with the id "templateText". Depending on when this EL is evaluated, different values will be returned. Therefore, is is crucial, that in order to evaluate the RichOutputText.getValue() method, that the call is made while that component is "in scope," or "in context."

Putting a component into context

Components are automatically put into context when they are being validated, updated, broadcasting events, rendering, etc. Sometimes it is necessary to interact with a component outside of its context. For example, perhaps you have backing bean code that wishes to get the input text value. This may be done by knowing a component's client ID, and using the visitTree method. Here is an example:

public void performAction(ActionEvent evt)
{
  String clientId = "template1:table1:0:firstNameInputText";
  FacesContext facesContext = FacesContext.getCurrentInstance();
  VisitContext visitContext = VisitContext.createVisitContext(facesContext,
    Collections.singleton(clientId), EnumSet.of(SKIP_UNRENDERED));
  GetFirstNameCallback callback = new GetFirstNameCallback();
  facesContext.getViewRoot().visitTree(visitContext, callback);

  String firstName = callback.getFirstName();
  ...
}

private static class GetFirstNameCallback
  implements VisitCallback
{
  private String _firstName;

  public String getFirstName()
  {
    return _firstName;
  }

  public VisitResult visit(
    VisitContext visitContext,
    UIComponent  target)
  {
    RichInputText inputText = (RichInputText)target;
    _firstName = (String)inputText.getValue();
    return VisitResult.COMPLETE;
  }
}

Here the code illustrates how to access the input text value for a component in the first row of a table.

Summary

Due to the fact that EL is contextual, it is important that any EL based functionality is only accessed in the correct context for that EL. As such, JSF developers should use caution when interacting with components via their Java APIs. Using visit tree should always be considered when trying to access properties from a component in order to ensure that the expected EL context has been setup when the component attempts to access its data. Failing to do so may cause issues with your programs, and even data contamination.

No comments: