1

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>
Kukeltje
  • 12,223
  • 4
  • 24
  • 47
Yeray Rodriguez
  • 307
  • 5
  • 14
  • However, if I use **varStatus.current** instead of **var**, the result is correct. I thought **varStatus.current** was equivalent to **var**. – Yeray Rodriguez Dec 04 '19 at 16:11
  • My guess is that extractFirst is called twice, because of some JSF evaluation. Look how you use extractFirst. – Joop Eggen Dec 04 '19 at 16:22
  • Hi @JoopEggen, that is not the problem. I have debugged the code and extractFirst is called only once. Morever, I don't understand why but the value of varStatus.current is correct instead of var – Yeray Rodriguez Dec 04 '19 at 16:29
  • 1: use the jsf 2.2+ namespace for `xmlns:c="http://java.sun.com/jsp/jstl/core"`, see https://stackoverflow.com/questions/7593603/jstl-xmlns-namespace-differences-between-jsf-1-2-and-jsf-2-x 2: don't use `prependId="false"`, see https://stackoverflow.com/questions/7415230/uiform-with-prependid-false-breaks-fajax-render 3: debug by setting breakpoints – Kukeltje Dec 04 '19 at 17:57
  • Does this answer your question? [JSTL in JSF2 Facelets... makes sense?](https://stackoverflow.com/questions/3342984/jstl-in-jsf2-facelets-makes-sense) – Selaron Dec 04 '19 at 20:33
  • @Kukeltje we also have an attempt to AJAX-Update c:forEach here which frequently causes issues. Similar questions in past where regularly linked to or closed as duplicate of https://stackoverflow.com/questions/3342984/jstl-in-jsf2-facelets-makes-sense - (at least [the linked github source](https://github.com/yerayrodriguez/nestedForeachProblem/blob/master/src/main/webapp/index.xhtml) does attempt an ajax update.) – Selaron Dec 04 '19 at 20:36
  • @Selaron: You cannot ajax-update a `c:foreach`, never. It is parsed/processed in view build time and not present in the view that is to be rendered or updated. The link you refer to contains all info. And the linked source (should be inline and is different from the inline code) does NOT attempt an ajax update. `` contains 2 errors (invalid, useless attributes) where OP most likely got confused with the `p:commandButton` and the code above does a full post/get so no issue related to that – Kukeltje Dec 04 '19 at 21:58
  • @Kukeltje while it is true there's no ajax involved here, we still have the problem that the viewState ist restored on postback instead of building a new view. The view is restored, then model is updated and view is rendered so JSTL-Tags get no chance to get work done on the new model state. I also tested this enabling AJAX and the symptomps are the same. The lifecycle also is the same in both cases (AJAX / nonAJAX), output from a phaseListener: RESTORE_VIEW 1 APPLY_REQUEST_VALUES 2 PROCESS_VALIDATIONS 3 UPDATE_MODEL_VALUES 4 INVOKE_APPLICATION 5 RENDER_RESPONSE 6 – Selaron Dec 05 '19 at 08:42
  • Long story short, the OP violates "Do not rely on JSF events in JSTL tag attributes" from the linked post. What do you think? – Selaron Dec 05 '19 at 08:44
  • @Kukeltje I have corrected and improved the example following your comments. Now linked and inline code are synchronized (I was doing some tests and I upload the wrong code) and you can do ajax and non ajax requests, also with Primefaces. – Yeray Rodriguez Dec 05 '19 at 10:46
  • @Selaron Unfortunately I have not found a solution. This is obviously a simplification of the real problem. I have to use c:forEach instead of ui:repeat because there are some ui:include nested to the c:forEach to generate dynamic content. This is not possible with ui:repeat. – Yeray Rodriguez Dec 05 '19 at 10:49
  • Care to explain the _"because there are some ui:include nested to the c:forEach to generate dynamic content. This is not possible with ui:repeat."_? Or at least refer to questions with answers that substantiate this. – Kukeltje Dec 05 '19 at 20:55

1 Answers1

4

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.

Kukeltje
  • 12,223
  • 4
  • 24
  • 47