12

I'm writing my custom table composite component with Mojarra JSF. I'm also trying to bind that composite to a backing component. The aim is to be able to specify the number of elements the table has in a composite attribute, later on the bound backing component will autogenerate the elements itself before view gets rendered. I've this sample code:

Main page:

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:comp="http://java.sun.com/jsf/composite/comp">
<h:head />
<body>
    <h:form>
        <comp:myTable itemNumber="2" />
    </h:form>
</body>
</html>

myTable.xhtml:

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:composite="http://java.sun.com/jsf/composite"
    xmlns:h="http://java.sun.com/jsf/html">

<h:body>
    <composite:interface componentType="components.myTable">
        <composite:attribute name="itemNumber" 
            type="java.lang.Integer" required="true" />
    </composite:interface>

    <composite:implementation>
        <h:dataTable value="#{cc.values}" var="value">
            <h:column headerText="column">
                #{value}
                <h:commandButton value="Action" action="#{cc.action}" />
            </h:column>
        </h:dataTable>
    </composite:implementation>
</h:body>
</html>

MyTable.java:

@FacesComponent("components.myTable")
public class MyTable extends UINamingContainer {

    private List<String> values = new ArrayList<String>();

    public void action() {
        System.out.println("Called");
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        // Initialize the list according to the element number
        Integer num = (Integer) getAttributes().get("itemNumber");
        for (int i = 0; i < num; i++) {
            values.add("item" + i);
        }
        super.encodeBegin(context);
    }

    public List<String> getValues() {
        return values;
    }

}

The issue is table gets rendered properly (in this case with two items), but action method doesn't get called when pressing the button on the lines.

If I follow the wiki page for composite components, I can get it work in that way, but having to initialize the List each time getValues() is called, introducing logic into the getter method :-(.

Any idea about that? It seems to be a trouble related with overriding encodeBegin method. I also tried initializing it on markInitialState, but attributes are not yet available there...


Tested with Mojarra 2.1.27 + Tomcat 6-7 & Mojarra 2.2.5 + Tomcat 7

Community
  • 1
  • 1
Xtreme Biker
  • 28,480
  • 12
  • 120
  • 195

2 Answers2

20

As to the cause, UIComponent instances are inherently request scoped. The postback effectively creates a brand new instance with properties like values reinitialized to default. In your implementation, it is only filled during encodeXxx(), which is invoked long after decode() wherein the action event needs to be queued and thus too late.

You'd better fill it during the initialization of the component. If you want a @PostConstruct-like hook for UIComponent instances, then the postAddToView event is a good candidate. This is invoked directly after the component instance is added to the component tree.

<cc:implementation>
    <f:event type="postAddToView" listener="#{cc.init}" />
    ...
</cc:implementation>

with

private List<String> values;

public void init() {
    values = new ArrayList<String>();
    Integer num = (Integer) getAttributes().get("value");

    for (int i = 0; i < num; i++) {
        values.add("item" + i);
    }
}

(and remove the encodeBegin() method if it isn't doing anything useful anymore)

An alternative would be lazy initialization in getValues() method.

BalusC
  • 992,635
  • 352
  • 3,478
  • 3,452
  • Thank you @BalusC, once again. Didn't know that `UIComponent` were stateless, but definitelly it makes sense. As a solution to keep the state, I've gone with a `List` reference which is created by a `@ViewScoped` managed bean and shared with the component itself ;-) – Xtreme Biker Feb 03 '14 at 12:13
  • That workaround seems to have a drawback. I usually initialize my managed bean stuff with a `preRenderView` method. I don't use `@PostConstruct` since I deal with view params that have to be set before to determine what to load. `postAddToView` seems to be called before that, so my managed bean hasn't yet that data to be shown. Is there another event I could use instead of that? – Xtreme Biker Feb 03 '14 at 12:39
  • `preRenderView` should be okay as long as you skip it when `FacesContext#isPostback()` is `true`. The bean is view scoped, right? – BalusC Feb 03 '14 at 12:41
  • Using a `preRenderView` event results in two intances of the component backing being called: after one is initialized the other's getter is called, returning `null` always. So I finally have gone with a lazy initialization at the getter method. Still have to test it more deeply, will try to provide a basic case if I replay the issue in my test context. – Xtreme Biker Feb 04 '14 at 08:05
  • @BalusC - It seemed simpler to make `values` part of the components state using `getStateHelper()`. As in my answer, after modifying `encodeBegin` and `getValues` to store and retrieve from cache, the component seems to be working correctly. – YoYo Mar 08 '19 at 15:43
1

A simpler solution would be to store and retrieve values as part of the components state. Storing can happen during encodeBegin, and retrieving could directly happen within the getter:

@FacesComponent("components.myTable")
public class TestTable extends UINamingContainer {
    public void action() {
        System.out.println("Called");
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        // Initialize the list according to the element number
        List<String> values = new ArrayList<>();
        Integer num = (Integer) getAttributes().get("itemNumber");
        for (int i = 0; i < num; i++) {
            values.add("item" + i);
        }
        getStateHelper().put("values",values);
        super.encodeBegin(context);
    }

    public List<String> getValues() {
        return (List<String>)getStateHelper().get("values");
    }
}

To avoid repeating the logic in getValues(), there could be additional parsing required in more complex cases, there should be a way to process and cache the attributes right after they become available, although I am not sure when and how at this point.

Either way - this seemed to be the simplest way to solve this problem.

YoYo
  • 7,796
  • 8
  • 50
  • 67