Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Composite Component inside ui:repeat: How to correctly save component state

Tags:

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() );
    }
}
like image 507
frifle Avatar asked Nov 26 '19 08:11

frifle


1 Answers

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.

like image 78
BalusC Avatar answered Oct 18 '22 04:10

BalusC