Search code examples
jsfjsf-2jstl

Wrong var value after updating nested c:forEach


I have found some strange behaviour when updating nested c:forEach. Apparently value of var is not correct on inner c:forEach.

The following example declares a simple Child Class, a Parent Class which includes a list of Child, and a managed bean (ViewScoped). This bean initializes a Parent (P0) with no children and a Parent (P1) with 3 children. The method extractFirst() simply get the first child available of P1 and add it to P0.

The index.html prints all the information using 2 c:forEatch nested tags. After submit, extractFirst() is executed and screen is updated, but, the result is not what I expected.

I get the same result with ajax and non-ajax requests with both standard h:commandButton and Primefaces p:commandButton.

First Scren

  • parent.id: P0
  • parent.id: P1
    • child.id: C0 - childIndex.current.id: (C0)
    • child.id: C1 - childIndex.current.id: (C1)

Second Scren (After submit)

  • parent.id: P0
    • child.id: C1 - childIndex.current.id: (C0) //Expected C0 but I get C1
  • parent.id: P1
    • child.id: - childIndex.current.id: (C1) //Expected C1 but I get ¿null?

Environment: Java8, JEE7, Wildfly 10.0.1

Code example (Full code at https://github.com/yerayrodriguez/nestedForeachProblem):

Child Class

public class Child implements Serializable {
  private static final long serialVersionUID = 1L;
  private String id;

  public Child(String id) {
    this.id = id;
  }

  public String getId() {
    return id;
  }
}

Parent Class

public class Parent implements Serializable {
  private static final long serialVersionUID = 1L;
  private String id;
  private List<Child> children = new ArrayList<>();

  public Parent(String id) {
    this.id = id;
  }

  public String getId() {
    return id;
  }

  public List<Child> getChildren() {
    return children;
  }
}

Managed Bean

@Named
@ViewScoped
public class TestManager implements Serializable {
  private static final long serialVersionUID = 1L;

  private List<Parent> root = new ArrayList<>();

  public List<Parent> getRoot() {
    return root;
  }

  @PostConstruct
  public void init() {
    // Parent 0 with no children
    Parent parent0 = new Parent("P0");
    root.add(parent0);
    // Parent 1 with 2 children
    Parent parent1 = new Parent("P1");
    parent1.getChildren().add(new Child("C0"));
    parent1.getChildren().add(new Child("C1"));
    root.add(parent1);
  }

  public String extractFirst() {
    Parent P0 = root.get(0);
    Parent P1 = root.get(1);
    if (!P0.getChildren().isEmpty()) {
      return null;
    }
    // Get first child of P1
    Child removedChild = P1.getChildren().remove(0);
    System.out.println("Removed child from P1: " + removedChild.getId()); // OK
    System.out.println("Is removed child id equals 'C0': " + removedChild.getId().equals("C0")); // OK
    // Add this child to P0
    P0.getChildren().add(removedChild);
    Child firstP0Child = P0.getChildren().get(0);
    System.out.println("Frist P0 Child: " + firstP0Child.getId()); // OK
    System.out.println("Is first P0 child id equals 'C0': " + firstP0Child.getId().equals("C0")); // OK
    return null;
  }

}

index.html

<!DOCTYPE html>
<html
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:p="http://primefaces.org/ui"
    xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<h:head />
<h:body>
    <h:form id="myForm">
        <h:commandButton action="#{testManager.extractFirst()}" value="NON AJAX" />
        <h:commandButton value="AJAX" action="#{testManager.extractFirst()}">
            <f:ajax render="myForm" />
        </h:commandButton>
        <p:commandButton action="#{testManager.extractFirst()}" value="PF NON AJAX" ajax="false" />
        <p:commandButton action="#{testManager.extractFirst()}" value="PF AJAX" update="myForm" />
        <ul>
            <c:forEach var="parent" items="#{testManager.root}">
                <li>parent.id: #{parent.id}</li>
                <ul>
                    <c:forEach var="child" items="#{parent.children}" varStatus="childIndex">
                        <li>child.id: #{child.id} - childIndex.current.id: (#{childIndex.current.id})</li>
                    </c:forEach>
                </ul>
            </c:forEach>
        </ul>
    </h:form>
</h:body>
</html>

Solution

  • Like mentioned in the comments and several referred links

    "you cannot update a for each"

    The view is build once and since you return null at the end of the action method, you effectively stay on the excact same view INSTANCE without actually rebuilding it. But since you did manipulate some backing data, It might to you seem you get wrong data, but you actually end up in some undefined state (at least that is my impression, it might even differ between JSF implementations and/or versions and maybe even the JSTL implementation, regarding the var and varStatus things, even maybe some view tree things).

    Even if you return an empty string at the end of the method the results are not as you would hope. Although the results (in the wildfly 10 I currently have at hand at least) are not as what I would have expected either. According to Difference between returning null and "" from a JSF action, I would have expected the bean to be recreated and the net result being that the page would look the same as when you started. Most likely the EL in the JSTL is 'cached' in some way that even in this case the results are undefined, substantiating the 'undefined' behaviour when manipulating backend data belonging to JSTL generated content.

    See e.g. what happens when you use 5 elements to P1 and move the 3rd from P1 to P0 on the action (you'll see even more remarkable things when at the same time changing the id of the 3rd element to e.g. append '-moved' to it (-m in my code and screenshot)

    Child removedChild = P1.getChildren().remove(3);
    removedChild.setId(removedChild.getId()+"-m");
    P0.getChildren().add(removedChild);
    

    enter image description here

    or even do that twice

    Child removedChild = P1.getChildren().remove(3);
    removedChild.setId(removedChild.getId()+"-m");
    P0.getChildren().add(removedChild);
    removedChild = P1.getChildren().remove(3);
    removedChild.setId(removedChild.getId()+"-m");
    P0.getChildren().add(removedChild);
    

    enter image description here

    So as you can see, not only the var is wrong but the varStatus is as well. You just did not notice since you moved 'C0'.

    Now how can the view be rebuild? Returning "index?faces-redirect=true" resulted in the page being rebuild, but unfortunately (but as expected) so was the bean with an net result of a 'no-op'. This can be solved by giving the bean a longer scope than the view. I personally only had @SessionScope at hand, but a DeltaSpike @ViewAccessScope would be shorter scoped and better 'managed' choice, How to choose the right bean scope?, I use it a lot.

    So the advice still is (always was but maybe sometimes hidden or not explicitly formulated):

    Don't manipulate data backing JSTL tags when the data has been used to create the view (tree, repeats) etc unless the scope of the bean backing the data is longer than @ViewScoped and a Post-Redirect-Get (PRG) is done to rebuild the view.


    Disclamer There might be things wrong in my 'undefined state' assumption. There might be clear explanation about the behaviour that is experienced, I just did not find it in the short time I looked into it, nor did I have the incentive to dig deeper. Since the 'right way' to do it is hopefully more clear.