JSF ui:repeat included by ui:include wrapped in h:

2019-02-19 00:50发布

问题:

Original question is below, but as I have come up with a more minimal example to demonstrate this problem, and figured it should go at the top.

Anyway, it appears that ui:repeat tags are processed before checking to see if parent elements are actually rendered. To recreate this, here is the facelet (minimalTest.xhtml):

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
<h:head>
  <title>Test JSF &lt;ui:repeat&gt; inside &lt;h:panelGroup rendered=&quot;false&quot;&gt;</title>
</h:head>
<h:body>
  <h:form>
    <h1>Testing</h1>
    <h:panelGroup rendered="false">
      <span>#{minimalTestBean.alsoThrowsException}</span>
      <ul>
        <ui:repeat value="#{minimalTestBean.throwsException}" var="item">
          <li>#{item}</li>
        </ui:repeat>
      </ul>
    </h:panelGroup>
  </h:form>
</h:body>
</html>

With using this bean (MinimalTestBean.java):

package com.lucastheisen.beans;


import java.io.Serializable;
import java.util.List;


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


@ManagedBean
@ViewScoped
public class MinimalTestBean implements Serializable {
    private static final long serialVersionUID = 9045030165653014015L;

    public String getAlsoThrowsException() {
        throw new RuntimeException( "rendered is false so this shouldnt get called either" );
    }

    public List<String> getThrowsException() {
        throw new RuntimeException( "rendered is false so this shouldnt get called" );
    }
}

From this example you can see that the h:panelGroup that contains the ui:repeat is statically set to rendered=false which I would assume would mean that none of the EL expressions inside of that h:panelGroup would get executed. The EL expressions just call getters which throw a RuntimeException. However, the ui:repeat is actually calling the getter for its list thus causing the exception even though it should not be getting rendered in the first place. If you comment out the ui:repeat element, no exceptions get thrown (even though the other EL expression remains in the h:panelGroup) as I would expect.

Reading other questions here on stackoverflow leads me to believe that is likely related to the oft-referred-to chicken/egg issue, but I am not sure exactly why, nor what to do about it. I imagine setting the PARTIAL_STATE_SAVING to false might help, but would like to avoid the memory implications.

---- ORIGINAL QUESTION ----

Basically, I have a page that conditionally renders sections using <h:panelGroup rendered="#{modeXXX}"> wrapped around <ui:include src="pageXXX.xhtml" /> (per this answer). The problem is that if one of the pageXXX.xhtml has a <ui:repeat> inside of it, it seems to get processed even when the containing <h:panelGroup> has rendered=false. This is a problem because some of my sections rely on having been initialized by other sections that should be visited before them. Why is the included pageXXX.xhtml getting processed?

This is a painful bug and incredibly hard to boil down to a small example, but here is the most minimal case I could build that demonstrates the issue. First a base page:

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
<h:head>
  <title>Test JSF &lt;ui:include&gt;</title>
</h:head>
<h:body>
  <h:form>
    <h1>#{testBean.title}</h1>
    <h:panelGroup rendered="#{testBean.modeOne}">
      <ui:include src="modeOne.xhtml" />
    </h:panelGroup>
    <h:panelGroup rendered="#{testBean.modeTwo}">
      <ui:include src="modeTwo.xhtml" />
    </h:panelGroup>
  </h:form>
</h:body>
</html>

As you can see this page will conditionally include either the modeOne page or the modeTwo page based upon the value in the testBean bean. Then you have modeOne (the default):

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
<ui:composition>
  <span>Okay, I&apos;m ready.  Take me to </span>
  <h:commandLink action="#{testBean.setModeTwo}">mode two.</h:commandLink>
</ui:composition>
</html>

Which in my real world app would be a page that sets up things needed by modeTwo. Once set up, an action on this page will direct you to modeTwo:

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core">
  <ui:composition>
    <div>Here is your list:</div>
    <ui:repeat value="#{testBeanToo.list}" var="item">
      <div>#{item}</div>
    </ui:repeat>
  </ui:composition>
</html>

The modeTwo page basically presents a details for the modeOne page in a ui:repeat as the actual information is in a collection. The main managed bean (TestBean):

package test.lucastheisen.beans;


import java.io.Serializable;


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


@ManagedBean
@ViewScoped
public class TestBean implements Serializable {
    private static final long serialVersionUID = 6542086191355916513L;
    private Mode mode;
    @ManagedProperty( value="#{testBeanToo}" )
    private TestBeanToo testBeanToo;

    public TestBean() {
        System.out.println( "constructing TestBean" );
        setModeOne();
    }

    public String getTitle() {
        System.out.println( "\ttb.getTitle()" );
        return mode.getTitle();
    }

    public boolean isModeOne() {
        return mode == Mode.One;
    }

    public boolean isModeTwo() {
        return mode == Mode.Two;
    }

    public void setModeOne() {
        this.mode = Mode.One;
    }

    public void setModeTwo() {
        testBeanToo.getReadyCauseHereICome();
        this.mode = Mode.Two;
    }

    public void setTestBeanToo( TestBeanToo testBeanToo ) {
        this.testBeanToo = testBeanToo;
    }

    private enum Mode {
        One("Mode One"),
        Two("Mode Two");

        private String title;

        private Mode( String title ) {
            this.title = title;
        }

        public String getTitle() {
            return title;
        }
    }
}

Is the bean for all the main data, and the TestBeanToo bean would be for the details:

package test.lucastheisen.beans;


import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;


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


@ManagedBean
@ViewScoped
public class TestBeanToo implements Serializable {
    private static final long serialVersionUID = 6542086191355916513L;
    private ObjectWithList objectWithList = null;

    public TestBeanToo() {
        System.out.println( "constructing TestBeanToo" );
    }

    public String getTitle() {
        System.out.println( "\ttb2.getTitle()" );
        return "Test Too";
    }

    public List<String> getList() {
        System.out.println( "\ttb2.getList()" );
        return objectWithList.getList();
    }

    public void getReadyCauseHereICome() {
        System.out.println( "\ttb2.getList()" );
        objectWithList = new ObjectWithList();
    }

    public class ObjectWithList {
        private List<String> list;

        public ObjectWithList() {
            list = new ArrayList<String>();
            list.add( "List item 1" );
            list.add( "List item 2" );
        }

        public List<String> getList() {
            return list;
        }
    }
}

回答1:

<ui:repeat> does not check the rendered attribute of itself (it has actually none) and its parents when the view is to be rendered. Consider using Tomahawk's <t:dataList> instead.