Search code examples
jsfview-scope

Why is this ViewScoped managed bean not working and how can I make it work?


This is the list page I have:

<h:dataTable value="#{actorSearchBacking.all}" var="actor">
    <h:column>
        <f:facet name="header">
            First Name
        </f:facet>
        #{actor.firstname}
    </h:column>
    <h:column>
        <f:facet name="header">
            Last Name
        </f:facet>
        #{actor.lastname}
    </h:column>
    <h:column>
        <h:form>
            <h:commandButton value="Update Actor" action="pocdetail">
                <f:setPropertyActionListener target="#{actorFormBacking.stupidActor}" value="#{actor}"/>
            </h:commandButton>
        </h:form>
    </h:column>
</h:dataTable>

which looks something like this in my local environment:

Actor List Page

This is pocdetail.xhtml which is the action of Update Actor button:

<h:body>
    <h:form id="updateActorForm"
            prependId="false">
        <h:inputText id="firstname" value="#{actorFormBacking.stupidActor.firstname}"/>
        <h:inputText id="lastname" value="#{actorFormBacking.stupidActor.lastname}"/>
        <h:commandButton id="updateActorButton"
                         value="Update Actor!"
                         action="#{actorFormBacking.updateActor()}"/>
    </h:form>
</h:body>

And finally ActorFormBacking is as follows:

@ManagedBean
@ViewScoped
public class ActorFormBacking implements Serializable {

    private Actor stupidActor;

    public Actor getStupidActor() {
        return stupidActor;
    }

    public void setStupidActor(Actor stupidActor) {
        this.stupidActor = stupidActor;
    }
}

When I debug the application, I see that setStupidActor is called and property stupidActor is set, but then when getter is called, it is again null.

Since this is a ViewScoped bean, I am expecting the stupidActor not to be null and I expect to see the pocdetail.xhtml page to be filled with values, but all I see is empty input texts since stupidActor is null.

What is it that I am missing? Why is the ViewScoped bean created again and the property is null?

Btw, I am using the annotations from the packages:

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;

Solution

  • It appears that you're navigating from one view to another view. In other words, you destroy the current view and create a new view. Logically, the view scope will also get destroyed and newly created, including all view scoped managed beans. That the view scoped managed bean happens to be referenced by both views doesn't change this behavior.

    A view scoped bean lives as long as the view itself. Like as that a request scoped bean lives as long as the request itself and so on. In order to have a better understanding of the lifetime of various scopes in JSF (and CDI), head to this Q&A: How to choose the right bean scope?

    The functional requirement is however understood. You want separate master-detail pages and pass the selected item from the master page to the detail page for editing. There are several ways to achieve this:

    1. The canonical way is to just use a bookmarkable GET link instead of an unbookmarkable POST link. Replace the below piece

      <h:form>
          <h:commandButton value="Update Actor" action="pocdetail">
              <f:setPropertyActionListener target="#{actorFormBacking.stupidActor}" value="#{actor}"/>
          </h:commandButton>
      </h:form>
      

      by this

      <h:link value="Update Actor" outcome="pocdetail">
          <f:param name="stupidActor" value="#{actor.id}" />
      </h:link>
      

      and in the detail page, obtain the Actor by its identifier which is passed-in as query string parameter. This is fleshed out in detail in this Q&A: Creating master-detail pages for entities, how to link them and which bean scope to choose. A @FacesConverter(forClass) is very useful here.


    2. In case you want to stick to POST for some reason, then your best bet is storing it in the request scope.

      FacesContext.getCurrentInstance().getExternalContext().getRequestMap().put("stupidActor", stupidActor);
      

      and retrieve it in the @PostConstruct of the very same bean

      stupidActor = (Actor) FacesContext.getCurrentInstance().getExternalContext().getRequestMap().get("stupidActor");
      

    3. If you happen to use CDI, or are open to (which I strongly recommend though, JSF managed beans are already deprecated in JSF 2.3.0-m06, see also Backing beans (@ManagedBean) or CDI Beans (@Named)?), then consider using MyFaces CODI's @ViewAccessScoped. Beans with this scope will live as long as all postbacked views explicitly reference the bean. Once you navigate out with a GET, or when the navigated view doesn't anywhere reference that bean, then it will get destroyed.

      @Named
      @ViewAccessScoped
      public class ActorFormBacking implements Serializable {}
      

    4. Merge the both views into a single view with conditionally rendered master-detail sections. You can find a kickoff example in this Q&A: Recommended JSF 2.0 CRUD frameworks. Or if you happen to use PrimeFaces, How to show details of current row from p:dataTable in a p:dialog and update after save.