5

I have a custom component that implements UIInput and that needs to save some state info for later reuse in postback requests. Used standalone it works fine, but inside an <ui:repeat> the postback finds the saved state of the latest rendered row of data. The log output of an action call is

INFORMATION: myData is "third foo"
INFORMATION: myData is "third foo"
INFORMATION: myData is "third foo"
INFORMATION: ok action

where I would expect

INFORMATION: myData is "first foo"
INFORMATION: myData is "second foo"
INFORMATION: myData is "third foo"
INFORMATION: ok action

I understand that myComponent is a single instance inside of ui:repeat. So what is the best way to save component state so it is restored correctly for each row in the dataset?

My XHTML form:

<h:form>
    <ui:repeat var="s" value="#{myController.data}">
        <my:myComponent data="#{s}"/>
    </ui:repeat>

    <h:commandButton action="#{myController.okAction}" value="ok">
        <f:ajax execute="@form" render="@form"/>
    </h:commandButton>
</h:form>

My Bean:

@Named
@ViewScoped
public class MyController implements Serializable {

    private static final long serialVersionUID = -2916212210553809L;

    private static final Logger LOG = Logger.getLogger(MyController.class.getName());

    public List<String> getData() {
        return Arrays.asList("first","second","third");
    }

    public void okAction() {
        LOG.info("ok action");
    }
}

Composite component XHTML code:

<ui:component xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://xmlns.jcp.org/jsf/html"
  xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
  xmlns:cc="http://xmlns.jcp.org/jsf/composite">

  <cc:interface componentType="myComponent">
    <cc:attribute name="data"/>
  </cc:interface>

  <cc:implementation>
    <h:panelGrid columns="2">
      <h:outputLabel value="cc.attrs.data"/>
      <h:outputText value="#{cc.attrs.data}"/>
      <h:outputLabel value="cc.myData"/>
      <h:outputText value="#{cc.myData}"/>
    </h:panelGrid>
  </cc:implementation>
</ui:component>

Composite Component backing class:

@FacesComponent
public class MyComponent extends UIInput implements NamingContainer {

    private static final Logger LOG=Logger.getLogger(MyComponent.class.getName());

    public String calculateData() {
        return String.format("%s foo", this.getAttributes().get("data") );
    }

    public String getMyData() {
        return (String)getStateHelper().get("MYDATA");
    }

    public void setMyData( String data ) {
        getStateHelper().put("MYDATA", data);
    }

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        this.setMyData( calculateData() );
        super.encodeBegin(context);
    }

    @Override
    public void processDecodes(FacesContext context) {
        super.processDecodes(context);
        LOG.log(Level.INFO, "myData {0}", getMyData() );
    }
}
Selaron
  • 6,105
  • 4
  • 31
  • 39
frifle
  • 810
  • 5
  • 24
  • 2
    I'm not sure what "the correct way" is, I can think of some workarounds: 1) use `"MYDATA" + getClientId()` as state key **or** 2) use `getAttributes()` instead of `getStateHelper()` **or** 3) use `c:forEach` instead of `ui:repeat` (which results in totally different component tree as Kukeltje would note here ;p) but (3) does not help when using your component within dataTable. – Selaron Nov 26 '19 at 10:10
  • Thanks @Selaron, but Im really looking for the "correct way" in this particular case. `c:forEach` and `getAttributes` is no workaround in our case, using `getClientId()` is a nice workaround. But, really, this is a bit ugly, isnt it? – frifle Nov 26 '19 at 11:02
  • Yes it's ugly. I'm excited to see this answered either as I had to work around this up to now. – Selaron Nov 26 '19 at 11:12
  • The approach is strange and confusing. My first try would be to use `getStateHelper()` instead of `this.getAttributes()` because `getStateHelper()` will be aware of the iterating context. – BalusC Dec 05 '19 at 13:54
  • 1
    @BalusC Well, the behaviour of `getStateHelper()` is my problem. Here we have two requests. In the first one the component gets encoded and we put `myData` into the `stateHelper` using `encodeBegin`. In the second request, a postback, the component gets decoded. And in `processDecodes` the `stateHelper` presents us unexpected data. Thats my question: How to use the `stateHelper` the proper way such that it is aware of the iterator even in a postback request? From my expectation it should do so out of the box - but it doesnt. – frifle Dec 05 '19 at 18:44

1 Answers1

4

Just tried reproducing your issue and yes, now I get what you're after all. You just wanted to use the JSF component state as some sort of view scope for the calculated variables. I can understand that. The observed behavior is indeed unexpected.

In a nutshell, this is explained in this blog of Leonardo Uribe (MyFaces committer): JSF component state per row for datatables.

The reason behind this behavior is tags like h:dataTable or ui:repeat only save properties related with EditableValueHolder interface (value, submittedValue, localValueSet, valid). So, a common hack found to make it work correctly is extend your component from UIInput or use EditableValueHolder interface, and store the state you want to preserve per row inside "value" field.

[...]

Since JSF 2.1, UIData implementation has a new property called rowStatePreserved. Right now this property does not appear on facelets taglib documentation for h:dataTable, but on the javadoc for UIData there is. So the fix is very simple, just add rowStatePreserved="true" in your h:dataTable tag:

In the end, you have basically 3 options:

  1. Use UIInput#value instead of something custom like MYDATA

    As instructed by the abovementioned blog, just replace getMyData() and setMyData() by the existing getValue() and setValue() methods from UIInput. Your composite component already extends from it.

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        this.setValue(calculateData()); // setValue instead of setMyData
        super.encodeBegin(context);
    }
    
    @Override
    public void processDecodes(FacesContext context) {
        super.processDecodes(context);
        LOG.log(Level.INFO, "myData {0}", getValue() ); // getValue instead of getMyData
    }
    

    And equivalently in the XHTML implementation (by the way, the <h:outputText> is unnecessary here):

    <h:outputText value="#{cc.value}" /> <!-- cc.value instead of cc.myData -->
    

    However, this didn't really work when I tried it on Mojarra 2.3.14. It turns out that Mojarra's implementation of the <ui:repeat> indeed restores the EditableValueHolder state during restore view (yay!), but then completely clears out it during decode (huh?), turning this a bit useless. I'm frankly not sure why it is doing that. I have also found in Mojarra's UIRepeat source code that it doesn't do that when it's nested in another UIData or UIRepeat. So the following little trick of putting it in another UIRepeat attempting to iterate over an empty string made it work:

    <ui:repeat value="#{''}">
        <ui:repeat value="#{myController.data}" var="s">
            <my:myComponent data="#{s}" />
        </ui:repeat>
    </ui:repeat>
    

    Remarkably is that nothing of this all worked in MyFaces 2.3.6. I haven't debugged it any further.


  2. Replace <ui:repeat> by <h:dataTable rowStatePreserved="true">

    As hinted in the abovementioned blog, this is indeed documented in UIData javadoc. Just replace <ui:repeat> by <h:dataTable> and explicitly set its rowStatePreserved attribute to true. You can just keep using your MYDATA attribute in the state.

    <h:dataTable value="#{myController.data}" var="s" rowStatePreserved="true">
        <h:column><my:myComponent data="#{s}" /></h:column>
    </h:dataTable>
    

    This worked for me in both Mojarra 2.3.14 and MyFaces 2.3.6. This is unfortunately not supported on UIRepeat. So you'll have to live with a potentially unnecessary HTML <table> markup generated by the <h:dataTable>. It was during JSF 2.3 work however discussed once to add the functionality to UIRepeat, but unfortunately nothing was done before JSF 2.3 release.


  3. Include getClientId() in state key

    As suggested by Selaron in your question's comments, store the client ID along as key in the state.

    public String getMyData() {
        return (String) getStateHelper().get("MYDATA." + getClientId());
    }
    
    public void setMyData(String data) {
        getStateHelper().put("MYDATA." + getClientId(), data);
    }
    

    Whilst it's a relatively trivial change, it's awkward. This does not infer portability at all. You'd have to hesitate and think twice every time you implement a new (composite) component property which should be saved in JSF state. You'd really expect JSF to automatically take care of this.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • What about an `o:repeat` Primefaces did it once too for exactly thus reason iirc... might even be a solution too. I can give it a try... not sure though if it can be done implementation independent – Kukeltje Dec 07 '19 at 07:44
  • Hu, it feels more like a bug than a feature. Anyway thanks a lot for your efforts! Btw, do you know of some ongoing discussion of that issue for jsf 2.4? I think `rowStatePreserved` for `ui:repeat` is a must have. – frifle Dec 09 '19 at 07:53
  • I just gave `p:repeat` a try: It doesnt preserve rowstate either. – frifle Dec 09 '19 at 07:56
  • @kukeltje nope; p:repeat is just a fork of Mojarras ui:repeat, to provide bugfixes for PRO Users, which are still on old Mojarra versions on old application servers. – tandraschko Dec 11 '19 at 15:50