Ajax view updates doesn't work properly in custom components with dedicated java class

StackOverflow https://stackoverflow.com/questions/17330771

  •  01-06-2022
  •  | 
  •  

Question

I have a bigger problem than the one I'll describe here, but since it would be difficult to explain and debug my code, I created a smaller (and dumb) problem to reproduce the same error.

In my view I want to print a list of strings, this list is calculated based in an offset. In my java class I have a huge static array of Strings. To calculate my list of strings (that will be printed in the page) I copy the values in my static array to my list, starting from the value of "offset".

In my page I also have a button "Increment offset" that increments the value of "offset" via an ajax request and update the results (the list is recalculated based in the new offset).

My problem is: when I click the button "Increment offset", the server-side code is executed, the offset is incremented but a problem happens on the update: the text showing the current offset gets updated with the new value of "offset", but the same doesn't happen to the list of strings, it is updated with the old values, and the new values are considered just if I do a second request, the values calculated in the second request are considered just in a 3rd update, making the list of strings in the view always one request late.

See this image (open it in a new tab to so it larger): enter image description here

Let's see some code...

The button:

<h:form>
  <h:commandButton value="Increment offsets">
    <f:ajax listener="#{test.incrementOffset()}" render=":stringList" />
  </h:commandButton>
</h:form>

The list of Strings:

<h:panelGroup id="stringList">
  <h1>Offsets increment = #{test.offset}</h1>
  <comp:stringPrinter offset="#{test.offset}" />
</h:panelGroup>

There it is the component I talked about in the title. The list of strings will not be calculated nor printed by the page. It will be calculated and printed in a composite component.

Before going to the code of the composite component, here's the managed bean "Test", used in the view:

@ManagedBean(name="test")
@ViewScoped
public class Test implements Serializable {
  private int offset;

  public int getOffset() {
    return offset;
  }

  public void incrementOffset(){
     offset++;
  }
}

The composite component: simple as I said, it just receives "offset" as parameter and print a list of strings. See the view code below:

<h:body>
  <composite:interface componentType="stringPrinter">
    <composite:attribute name="offset" type="java.lang.Integer" required="true" />
  </composite:interface>
  <composite:implementation>
    <ul>
      <ui:repeat value="#{cc.list}" var="string">
        <li>#{string}</li>
      </ui:repeat>
    </ul>
  </composite:implementation>
</h:body>

And this is how the list of strings is calculated:

@FacesComponent(value="stringPrinter")
public class StringPrinter extends UINamingContainer implements Serializable {
  private ArrayList<String> list;

  private static String[] LIPSUM = "...".split(" "); // "..." is not the actual string, the string is irrelevant (it has more than 400 words separated by spaces).
  private static int ARRAY_SIZE = 12;

  private void generateList(){
    int offset = (Integer) getAttributes().get("offset");
    int position = offset;
    list = new ArrayList<String>();
    for (int i = 0; i < ARRAY_SIZE; i++){
      list.add(LIPSUM[position++ % LIPSUM.length]);
    }
  }

  public ArrayList<String> getList() {
    if (null == list) generateList();
      return list;
    }
  }
}

The problem doesn't happen if I don't use a component. The problem doesn't happen when the list is calculated in the parent view and the component has no java class attached. But in my real problem I don't need to print Strings, I really need things to get processed by a composite-component, making it necessary to have a dedicated class for it.


Some debugging:

The ui:repeat tag could be the problem. If instead of printing a list (which would require an ui:repeat) I needed to print the first String in the list, everything would get correctly updated, in the correct time. See the code of the composite component without the use of ui:repeats:

<h:body>
  <composite:interface componentType="stringPrinter">
    <composite:attribute name="offset" type="java.lang.Integer" required="true" />
  </composite:interface>
  <composite:implementation>
    <h:outputText value="#{cc.list.get(0)}" />
  </composite:implementation>
</h:body>

The code above works, but it's not what I need to do, I really need to use ui:repeat. I also tried c:forEach, same result.

For further debugging I made myself the question: "does the method getList(), in the component, get called after the method incrementOffset(), in the page, is executed?". This is the log I got:

*** GET LIST CALLED!
*** GET LIST CALLED!
*** OFFSET INCREMENTED!
*** GET LIST CALLED!
*** GET LIST CALLED!

I used System.out.flush() to prevent messages from being buffered. It seems getList() is called after the method is executed, I don't see any reason for the view to be updated with old values. Anyway, the fact it is called two times just before incrementOffset() is executed and two times after is a bit strange.

This problem affects Mojarra 2.1.7, 2.1.22, 2.1.23 and 2.2.0. I did not test the other versions.

I'm gonna update this post with further debug.

To download the Maven project and test it for yourself: zip file

Thanks in advance for any answer.

Was it helpful?

Solution

I discovered where the problem is. I thought that when a component gets updated it was entirely reprocessed, but it doesn't happen in some occasions. For some reason, it seems that when ui:repeat is used, JSF tries to retard the component reset.

A solution is to explicitly recalculate the list when getList() is called:

It was like this before:

  public ArrayList<String> getList() {
    if (null == list) generateList();
      return list;
    }
  }

If I remove the verification for null it works.

In my opinion that's what was happening: when the component was updated the list still had the values calculated before, it was not null and no recalculation was done. In the time between one request and another, the component was getting reconstructed. With the component reseted, list was equal to null and the recalculation was done on getList(), making the view always one request late.


EDIT: A much better solution is to use f:event in the component's view instead of recalculating the list on every get.

<f:metadata>
  <f:event type="preRenderComponent" listener="#{cc.generateList}" />
</f:metadata>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top