更新嵌套后 var 值错误 c:forEach

Wrong var value after updating nested c:forEach

我在更新嵌套 c:forEach 时发现了一些奇怪的行为。显然 var 的值在内部 c:forEach 上不正确。

下面的例子声明了一个简单的ChildClass,一个ParentClass 其中包括 Child 的列表和一个托管 bean (ViewScoped)。这个 bean 初始化一个没有 children 的 Parent (P0) 和一个有 3 children 的 Parent (P1)。 extractFirst() 方法简单地获取 P1 的第一个 child 可用并将其添加到 P0。

index.html 使用 2 c:forEatch 嵌套标签打印所有信息。提交后,执行 extractFirst() 并更新屏幕,但是,结果不是我所期望的。

我在 ajax 和 non-ajax 标准 h:commandButton 和 Primefaces p:commandButton 请求中得到相同的结果。

第一屏

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

第二个屏幕(提交后)

  • 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?

环境:Java8、JEE7、Wildfly 10.0.1

代码示例(完整代码位于 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;
  }
}

托管 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>

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

"you cannot update a for each"

视图只构建一次,由于您 return null 在操作方法的末尾,您实际上 stay on the excact same view INSTANCE without actually rebuilding it。但是由于您 确实 操作了一些支持数据,在您看来您似乎得到了 错误的 数据,但实际上您最终处于某种未定义的状态(在至少这是我的印象,它甚至可能在 JSF 实现 and/or 版本之间甚至可能在 JSTL 实现之间有所不同,关于 var 和 varStatus 的东西,甚至可能是一些视图树的东西)。

即使您 return 在方法末尾添加一个空字符串,结果也不会如您所愿。尽管结果(至少在我目前手头的 wildfly 10 中)也不像我预期的那样。根据 Difference between returning null and "" from a JSF action,我希望重新创建 bean,最终结果是页面看起来与您开始时一样。 JSTL 中的 EL 很可能在某种程度上是 'cached',即使在这种情况下,结果也是未定义的,这证实了在处理属于 JSTL 生成的内容的后端数据时的 'undefined' 行为。

参见例如当你在 P1 中使用 5 个元素并将第三个元素从 P1 移动到 P0 时会发生什么(当同时更改第三个元素的 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" 导致页面被重建,但不幸的是(但正如预期的那样)最终结果为 'no-op' 的 bean 也是如此。这可以通过为 bean 提供比视图更长的作用域来解决。我个人手头只有 @SessionScope,但 DeltaSpike @ViewAccessScope 的范围更短,'managed' 的选择更好,How to choose the right bean scope?,我经常使用它。

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

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.


免责声明 我的 'undefined state' 假设可能有问题。可能对所经历的行为有明确的解释,我只是在短时间内没有找到它,也没有动力去深入挖掘。由于 'right way' 这样做希望更清楚。