JSF中的递归(c:forEach vs. ui:repeat)

7

我正在尝试通过JSF中的递归构建导航树。我定义了一个 navigationNode 组件:

<composite:interface>
    <composite:attribute name="node" />
</composite:interface>

<composite:implementation>
<ul>
    <ui:repeat value="#{navigationTreeBean.getChildrenForNode(cc.attrs.node)}" var="child">
        <li><navigation:navigationNode node="#{child}" /></li>
    </ui:repeat>
</ul>
</composite:implementation>

我的树被声明为:

rootNode = new DefaultMutableTreeNode(new NodeData("Dashboard", "dashboard.xhtml"), true);
DefaultMutableTreeNode configurationsNode = new DefaultMutableTreeNode(new NodeData("Configurations", "configurations.xhtml"), true);
rootNode.add(configurationsNode);

我通过以下方式调用组件:

<nav:navigationNode node="#{rootNode}" />

问题是,这会导致StackOverflowError
有一些关于在JSF中构建递归的参考资料(例如,c:forEach vs ui:repeat in Facelets)。常见问题似乎是混合了构建时和渲染时的组件/标签。在我的情况下:
- 我的复合组件实际上是一个标签,在构建树时执行 - ui:repeat是一个实际的JSF组件,在渲染树时评估
子组件navigation:navigationNode是否在ui:repeat组件之前处理?如果是,它使用哪个对象作为#{child}?它是null吗(似乎不是)?这里的问题是子组件实际上是在不关心ui:repeat的情况下创建的,因此每次创建一个新的子组件,即使它不一定需要。 c:forEach vs ui:repeat in Facelets文章有一个单独的部分(递归)用于解决此问题。建议使用c:forEach代替。我尝试了这个方法,但仍然给出了相同的StackOverflowError,有不同的跟踪信息,我无法理解。
我知道我们也可以通过扩展UIComponent来构建组件,但这种方法(在Java代码中编写html)似乎很丑陋。我宁愿使用MVC样式/模板。但是,如果没有其他方法,我必须将此类递归实现为UIComponent吗?
2个回答

5

JSF内置的声明性标签不适合处理这种递归。JSF构建了一个有状态的组件树,在请求之间保持不变。如果在随后的请求中恢复视图,则视图状态可能无法反映模型中的更改。

我更喜欢一种命令式方法。我认为你有两个选择:

  • 使用binding属性将控件(例如某种面板)绑定到提供UIComponent实例及其子级的后备bean上 - 您编写代码来实例化UIComponent并添加任何您想要的子级。请参见规范中的binding属性合同。
  • 编写自定义控件,实现以下内容之一:一个UIComponent;一个Renderer;一个标记处理程序;元数据文件(根据您正在进行的操作以及使用哪个版本的JSF删除适当的部分 - 您可以执行其中一些或全部操作)。

也许另一个选择是选择一个已经做到这一点的第三方控件。

更新:如果你正在使用非常有用的OmniFaces库(如果你还没有使用,应该使用),那么有<o:tree>这个标签,它完全没有HTML生成,但是专门设计支持像这样的用例。

<o:tree value="#{bean.treeModel}" var="item" varNode="node">
    <o:treeNode>
        <ul>
            <o:treeNodeItem>
                <li>
                    #{node.index} #{item.someProperty}
                    <o:treeInsertChildren />
                </li>
            </o:treeNodeItem>
        </ul>
    </o:treeNode>
</o:tree>

编辑:

这是一种基于模型的方法,不涉及编写自定义组件或生成组件树。它有点丑陋。

Facelets视图:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <h:head><title>Facelet Tree</title></h:head>
  <h:body>
    <ul>
      <ui:repeat value="#{tree.treeNodes}" var="node">
        <h:outputText rendered="#{node.firstChild}"
                value="&lt;ul&gt;" escape="false" />
        <li>
          <h:outputText value="#{node.value}" />
        </li>
        <ui:repeat rendered="#{node.lastChild and empty node.kids}"
            value="#{node.lastChildLineage}" var="ignore">
          <h:outputText
              value="&lt;/ul&gt;" escape="false" />
        </ui:repeat>
      </ui:repeat>
    </ul>
  </h:body>
</html>

托管的Bean:
@javax.faces.bean.ManagedBean(name = "tree")
@javax.faces.bean.RequestScoped
public class Tree {
  private Node<String> root = new Node(null, "JSF Stuff");

  @PostConstruct
  public void initData() {
    root.getKids().add(new Node(root, "Chapter One"));
    root.getKids().add(new Node(root, "Chapter Two"));
    root.getKids().add(new Node(root, "Chapter Three"));
    Node<String> chapter2 = root.getKids().get(1);
    chapter2.getKids().add(new Node(chapter2, "Section A"));
    chapter2.getKids().add(new Node(chapter2, "Section B"));
  }

  public List<Node<String>> getTreeNodes() {
    return walk(new ArrayList<Node<String>>(), root);
  }

  private List<Node<String>> walk(List<Node<String>> list, Node<String> node) {
    list.add(node);
    for(Node<String> kid : node.getKids()) {
      walk(list, kid);
    }
    return list;
  }
}

一棵树节点:
public class Node<T> {
  private T value;
  private Node<T> parent;
  private LinkedList<Node<T>> kids = new LinkedList<>();

  public Node(Node<T> parent, T value) {
    this.parent = parent;
    this.value = value;
  }

  public List<Node<T>> getKids() {return kids;}
  public T getValue() { return value; }

  public boolean getHasParent() { return parent != null; }

  public boolean isFirstChild() {
    return parent != null && parent.kids.peekFirst() == this;
  }

  public boolean isLastChild() {
    return parent != null && parent.kids.peekLast() == this;
  }

  public List<Node> getLastChildLineage() {
    Node node = this;
    List<Node> lineage = new ArrayList<>();
    while(node.isLastChild()) {
        lineage.add(node);
        node = node.parent;
    }
    return lineage;
  }
}

输出:

*  JSF Stuff
      o Chapter One
      o Chapter Two
            + Section A
            + Section B 
      o Chapter Three 

我仍然会硬着头皮编写自定义树形控件。


1
谢谢您的建议。我必须说,如果情况是这样的话,我有点失望JSF 2.0。仍在寻找其他观点,但可能我将不得不走这条路。我浏览了RichFaces和PrimeFaces,但它们似乎非常笨重,并且它们的样式似乎需要比我想要的更多的工作(我们已经有XHTML / CSS模板),因此我仍在寻找构建自定义导航树的方法。 - Tuukka Mustonen
PrimeFaces支持/使用jQuery themeroller CSS框架。普通的CSS设计师应该已经熟悉了它。 - BalusC
@Tuukka Mustonen - JSF框架下面的有状态组件树使得你想要的变得困难,而且如果移除它将会是一个破坏性的改变。 - McDowell
@ McDowell:感谢您提供示例代码。在此基础上构建似乎有些hackyish,但我也觉得很有趣:)从来没有想过。我觉得这只有在树以正确的顺序构建时才有效。不过,这不会是个问题。@ BalusC:感谢您指向themeroller。不过我担心它太简单了。每个组件也有自己的CSS,你不能在themeroller中自定义。不过,我会再看一下它,在我第一次浏览时,我没有找到如何覆盖组件特定的CSS。 - Tuukka Mustonen
1
尝试使用每个节点仅有一个子节点的树来测试您的算法。该树将在同一级别上呈现所有内容。 - Fabio Pigagnelli
@FabioPigagnelli 谢谢您指出这个问题!我已经进行了更正,但实现方式变得更加丑陋了。正如我在文本中所说的,这不是应该实现此类控件的方式。 - McDowell

4

在将我们的应用程序从jsf 1.x迁移到2.x时,我遇到了类似的问题(StackOverflowException)。如果您正在使用c:forEach方法进行jsf递归,请确保您正在使用jstl核心的新命名空间。请使用:

xmlns:c="http://java.sun.com/jsp/jstl/core"

代替
xmlns:c="http://java.sun.com/jstl/core"

这是我们使用的模式,根据您的情况进行了调整。
客户端.xhtml
<ui:include src="recursive.xhtml">
    <ui:param name="node" value="#{child}" />
</ui:include>

recursive.xhtml

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:c="http://java.sun.com/jsp/jstl/core" >
    <ul>
        <c:forEach items="#{node.children}" var="child">
            <li>
                #{child.label}
                <ui:include src="recursive.xhtml">
                    <ui:param name="node" value="#{child}" />
                </ui:include>
            </li>
        </c:forEach>
    </ul>   
</ui:composition>

我只是想澄清一下,在我们的情况下,递归是使用嵌套的ui:include/c:forEach实现的。 - Anonymous
问题提到了使用c:forEach时出现了StackoverflowException异常,这正是我在无效命名空间中遇到的问题。我更新了答案,包括递归的完整代码。 - Anonymous
这至少看起来是递归,而不是另一种解决方案,它似乎只是构建一个巨大的节点列表,并使用一些标志来指示节点是否为叶子节点。我认为另一种解决方案不会像递归那样有任何深度。 - K.Nicholas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接