首页 > 解决方案 > 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>

标签: jsfjsf-2jstl

解决方案


就像评论中提到的和几个引用的链接

“你不能为每个更新一个”

该视图是构建一次,并且由于您return null在操作方法结束时,您有效地停留在完全相同的视图实例上,而无需实际重建它。但是由于您确实操作了一些支持数据,在您看来您可能得到了错误的数据,但实际上您最终处于某种未定义的状态(至少这是我的印象,它甚至可能在 JSF 实现和/或版本之间有所不同,甚至可能JSTL 实现,关于 var 和 varStatus 的东西,甚至可能是一些视图树的东西)。

即使您在方法的末尾返回一个空字符串,结果也不是您希望的那样。虽然结果(至少在我目前手头的 wildfly 10 中)也不是我所期望的。根据从 JSF 操作返回 null 和 "" 之间的区别,我希望重新创建 bean,最终结果是页面看起来与您开始时相同。JSTL 中的 EL 很可能以某种方式“缓存”,即使在这种情况下,结果也是未定义的,从而证实了在处理属于 JSTL 生成内容的后端数据时的“未定义”行为。

例如,当您使用 5 个元素到 P1 并将第 3 个元素从 P1 移动到 P0 动作时会发生什么(当同时将第 3 个元素的 id 更改为例如附加“-moved”时,您会看到更显着的事情)到它(我的代码和屏幕截图中的-m)

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

在此处输入图像描述

甚至两次

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);

在此处输入图像描述

如您所见,不仅var错误,而且错误varStatus。你只是没有注意到,因为你移动了'C0'。

现在如何重建视图?返回"index?faces-redirect=true"导致页面被重建,但不幸的是(但正如预期的那样)bean 的最终结果为“无操作”。这可以通过给 bean 一个比视图更长的范围来解决。我个人只有@SessionScope手头,但 DeltaSpike 的@ViewAccessScope范围更短,更好的“管理”选择,如何选择正确的 bean 范围?,我经常使用它。

所以建议仍然是(总是但有时可能隐藏或未明确制定):

当数据已用于创建视图(树、重复)等时,请勿操作支持 JSTL 标记的数据,除非支持数据的 bean 的范围大于@ViewScoped并且完成后重定向获取 (PRG) 以重建风景。


免责声明 我的“未定义状态”假设可能有问题。关于所经历的行为可能有明确的解释,我只是在我调查它的短时间内没有找到它,我也没有动力去深入挖掘。由于这样做的“正确方法”希望更清楚。


推荐阅读