Search code examples
javajsfjsf-2java-ee-6myfaces

Dynamically rendered JSF 2 content in request scoped - listener not called when in dynamic content


I have put my problem case below, which simplifies a real world problem I am having. By changing the Bean to @SessionScoped I can solve this, but that is really undesirable. As is @ViewScoped. I think it should work in @RequestScoped. My questions are:

  • Why does JSF need access to #{simpleBean.outerStrings} to invoke the #{simpleBean.processInnerClick()} listener (outerStrings will have died in the previous request)
  • What is the best pattern for solving this?

Happy to edit to make clearer - just let me know.

Bean class

import javax.enterprise.context.RequestScoped;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

@Named
@RequestScoped
public class SimpleBean implements Serializable {

    private List<String> topLevelStrings = 
               Arrays.asList("TopLevel1", "TopLevel2");

    private List<String> outerStrings;

    private List<String> innerStrings;

    public void processOuterClick() {
        System.err.println("processOuterClick()");
        outerStrings = Arrays.asList("Outer1", "Outer2", "Outer3");
    }

    public void processInnerClick() {
        System.err.println("processInnerClick()");
        innerStrings = Arrays.asList("InnerA", "InnerB", "InnerC");
    }

    public List<String> getOuterStrings() {
        return outerStrings;
    }

    public List<String> getInnerStrings() {
        return innerStrings;
    }

    public List<String> getTopLevelStrings() {
        return topLevelStrings;
    }

}

XHTML

<h:form>
<ui:repeat var="toplevel" value="#{simpleBean.topLevelStrings}"
           id="toplevel_id">
<h:commandLink id="toplevel_command">
#{toplevel} <br/>
<f:ajax render="outerlevel_panel" 
        listener="#{simpleBean.processOuterClick()}"/>
</h:commandLink>
<h:panelGroup id="outerlevel_panel">
<ui:repeat var="outerLevel" value="#{simpleBean.outerStrings}" 
    id="outerlevel_id">
<h:commandLink id="outerlevel_command">
#{outerLevel} <br/> 
<f:ajax listener="#{simpleBean.processInnerClick()}" render="innerlevel_panel"/>                   
</h:commandLink>                            
<h:panelGroup id="innerlevel_panel">
<ui:repeat var="innerLevel" value="#{simpleBean.innerStrings}" 
           id="innerlevel_id">
<h:commandLink id="innerlevel_command">
#{innerLevel} <br/>        
</h:commandLink>
</ui:repeat>
</h:panelGroup>
</ui:repeat>
</h:panelGroup>         
</ui:repeat>
</h:form>

Basically:

  • The #{simpleBean.processOuterClick()} listener fires ok and the #{outerLevel} command links render
  • But when I click on the #{outerLevel} command Link the #{simpleBean.processInnerClick()} listener is never fired

Solution

  • The request scope is the wrong scope for this particular requirement. A request scoped bean is garbaged by end of the response and a new one will be created in the subsequent request with all of its properties set to default. In JSF2 terms, you really need the view scope. The session scope is indeed too broad and much worse for this requirement than the request scope (when an enduser opens the same page in multiple windows/tabs, they will share physically the one and same bean, resulting in unintuitive per-view behaviour when the enduser switches between views after interactions).

    All you need to do to fix your particular problem is to use @ManagedBean @ViewScoped:

    import javax.faces.bean.ManagedBean;
    import javax.faces.bean.ViewScoped;
    
    @ManagedBean
    @ViewScoped
    public class SimpleBean implements Serializable {
        // ...
    }
    

    As you seem to prefer CDI management over JSF management for some reason, the CDI alternative to @ViewScoped is @ConversationScoped.

    import javax.enterprise.context.Conversation;
    import javax.enterprise.context.ConversationScoped;
    import javax.inject.Named;
    
    @Named
    @ConversationScoped
    public class SimpleBean implements Serializable {
    
        @Inject
        private Conversation conversation;
    
        @PostConstruct
        public void init() {
            conversation.begin();
        }
    
        public String navigateToOtherPage() {
            conversation.end();
            return "otherPage?faces-redirect=true";
        }
    
        // ...
    }
    

    You only have to manage the begin and end of the conversation yourself.

    See also: