p:treenode 与 DefaultTreeNode 子类的奇怪行为

Posted

技术标签:

【中文标题】p:treenode 与 DefaultTreeNode 子类的奇怪行为【英文标题】:Strange behaviour of p:treenode with DefaultTreeNode subclass 【发布时间】:2014-08-12 13:25:57 【问题描述】:

我尝试在 PrimeFaces 4.0 中以动态模式与持久性 API 一起使用 p:tree。 我有 @Entity 类(Nomen),它已经有引用自身的元素。我尝试了这两种变体:在那个非常 Nomen 类接口 TreeNode 中实现并使其成为 DefaultTreeNode 的子类。最后一个变体扩展了 DefaultTreeNode。行为是一样的。

@Entity
@Table(name = "Nomen")
public class Nomen extends DefaultTreeNode implements Serializable 
@Id
private Integer id;
...
@Column(name = "Name")
private String name;
...
@JoinColumn(name = "Self_Id", referencedColumnName = "Id")
@ManyToOne
private Nomen parent;

@OneToMany(mappedBy = "parent", fetch=FetchType.EAGER)
private List<Nomen> nomenCollection;
...
public Nomen() 
    super();

...
@Override
public String getType() 
    if (nomenCollection == null) return "NomenLeaf";
    return "NomenGroup";


@Override
public Nomen getData() 
    Logger.getLogger(this.getClass().getName()).log(Level.INFO, "getData for 0", new Object[] this);
    return this;



@Override
public Nomen getParent() 
    return this.parent;

@Override
public void setParent(TreeNode tn) 
    if (!(tn instanceof Nomen)) 
        return;
    
    try 
        parent.removeChild(this);
    
    catch (NullPointerException e)  // Nothing to do
    if (tn != null) 
        ((Nomen)tn).addChild(this);
    
    parent = (Nomen)tn;


@Override
public int getChildCount() 
    if (nomenCollection == null) return 0;
    return nomenCollection.size();


@Override
public boolean isLeaf() 
    return nomenCollection == null;


@Override
public String getRowKey() 
    Logger.getLogger(this.getClass().getName()).log(Level.INFO, "getRowKey: this: 0 id: 1", new Object[] this, Integer.toString(getId()) );
    return Integer.toString(getId());

@Override
public List<TreeNode> getChildren() 
//       return new ArrayList<TreeNode> (nomenCollection);
//       return (List<TreeNode>)(List<? extends TreeNode>)nomenCollection;
    ArrayList<TreeNode> r = new ArrayList<> ();
    for (Nomen n : nomenCollection) 
        r.add(n);
    
    Logger.getLogger(this.getClass().getName()).log(Level.INFO, "getChildren for 0: List: 1 ", new Object[] this, r);
    return r;

public List<Nomen> getNomenCollection() 
    return nomenCollection;

public void setNomenCollection (List<Nomen> n) 
    nomenCollection = n;


Xhtml 文件非常简单,就像 PrimeFaces 示例一样:

<h:form id="myform">
    <p:tree id="ntree" value="#nomenPfCtl.root" var="item" selectionMode="single" selection="#nomenPfCtl.selected" datakey="id" dynamic="true">
        <p:treeNode type="NomenGroup" expandedIcon="ui-icon-folder-open" collapsedIcon="ui-icon-folder-collapsed">
            <h:outputText value="#item"/>
        </p:treeNode>
        <p:treeNode type="NomenLeaf" expandedIcon="ui-icon-document" collapsedIcon="ui-icon-document">
            <h:outputText value="#item"/>
        </p:treeNode>
    </p:tree>
</h:form>

p:tree 奇怪的无法解释的行为是它通常(使用 Nomen.toString)只呈现根节点的第二个(最后一个)子节点(id=2)。它呈现整个树形结构,可以用图标进一步扩展,但标签是空的。

下面是加载树的初始视图时提取的服务器协议,其中预计将呈现根 (id=2159) 的两个子项(id 为 1 和 2):

  1.getRoot: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура)]]
  2.getRowKey: this: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура) id: 2159]]
  3.getRowKey: this: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура) id: 2159]]
  4.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  5.getRowKey: this: AsupoksEntities.Nomen[ id=1 ] (Работы) id: 1]]
  6.getRoot: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура)]]
  7.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  8.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  9.getData for AsupoksEntities.Nomen[ id=2 ] (Материалы)]]
  10.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  11.getRowKey: this: AsupoksEntities.Nomen[ id=2 ] (Материалы) id: 2]]
  12.getRoot: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура)]]
  13.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  14.getRoot: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура)]]
  15.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  16.getRoot: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура)]]
  17.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  18.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  19.getData for AsupoksEntities.Nomen[ id=1 ] (Работы)]]
  20.getRoot: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура)]]
  21.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  22.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]
  23.getData for AsupoksEntities.Nomen[ id=2 ] (Материалы)]]
  24.getRoot: AsupoksEntities.Nomen[ id=2159 ] (Номенклатура)]]
  25.getChildren for AsupoksEntities.Nomen[ id=2159 ] (Номенклатура): List: [AsupoksEntities.Nomen[ id=1 ] (Работы), AsupoksEntities.Nomen[ id=2 ] (Материалы)] ]]

调用顺序看起来很奇怪。

    日志第 5 行没有调用 getData(预期调用 getData,然后调用 getRowKey,如第 9,11 行)

    对 ROOT 的 getChildren 调用过多(预期调用 getChildren,然后通过后续 getData 调用遍历 List。

我不明白出了什么问题。跟踪 getChildren 调用可以更深入地提供正确的树结构。集合被跟踪以被实例化。 (添加 FetchType.EAGER 没有任何改变)。我计划使用带有拖放功能的 p:tree 组件来实现层次编辑器,但现在停止。我不想在 DefaultTreeNode 中包含 Nomen 作为数据对象,然后复制实体类本身已经实现的自然树结构。然后同步两个父更改;-)

有人可以帮忙吗?

【问题讨论】:

【参考方案1】:

注释掉以下代码即可解决问题

/*
@Override
public String getRowKey() 
    Logger.getLogger(this.getClass().getName()).log(Level.INFO, "getRowKey: this: 0 id: 1", new Object[] this, Integer.toString(getId()) );
    return Integer.toString(getId());

*/

我当然不知道 p:tree 如何使用 getRowKey。很可能,它必须以某种方式对应于 p:tree 的 datakey 属性。我还没有深入研究它们应该如何协同工作。但是调用序列中的所有混乱都是由覆盖 getRowKey() 引起的。

感谢大家阅读!

【讨论】:

【参考方案2】:

挖掘 DefaultTreeNode 的来源会发现一些在可用文档中反映不佳的细微特征。

首先我的意思是特殊的(我认为是一般性的)用例:我有 @Entity 类,它已经包含引用自身的元素。我尝试为该类实现层次编辑器。几年前论坛中的某个人建议使用子类 DefaultTreeNode,我决定也这样做。文档中遗漏了一些重要的东西,我不得不研究 p:tree 和 TreeNode 的真实行为。如果有人需要,我在这里尝试描述这些微妙的功能。

    就我而言(PF4.0、JSF2.2、Netbeans8.0、Glassfish4.0),为 p:tree 添加可拖动属性是不够的。我必须添加 p:draggable for "thattree" 和 p:droppable

    <p:tree id="ntree" value="#treeController.root" var="nod" draggable="true" droppable="true" dynamic="true">
        <p:ajax event="dragdrop" listener="#treeController.onDragDrop" update="@this">
    ...
    </p:tree>
    <p:draggable for="ntree" handle=".ui-treenode-label, .ui-treenode-icon"/>
    <p:droppable for="ntree"/>
    

    线

    <p:ajax event="dragdrop" ... 
    

在前面的示例中设置侦听器是必需的。 p:tree 不会在 dragdropevent 上调用模型的 setParent 方法,这就是为什么我们需要侦听器在客户端视图中重新设置节点后调整支持 bean 中的树结构。 在那个监听器中,我们简单地调用模型的 setParent。必须注意,在 PF4.0 的文档中,“dragdrop”事件未列在 p:tree 的 ajax 事件中。

    如何继承 DefalutTreeNode。文档中遗漏了一个非常微妙的功能,只有挖掘源代码才能使其正常工作。

事实上,推荐给子类的 DefaultTreeNode 只做 getter 和 setter,它们实现了 TreeNode 接口。第一个技巧是在构造函数中:DefaultTreeNode 中的所有构造函数都使用新的 TreeNodeChildren(扩展 List)初始化成员 List 子项。当时 DefaultTreeNode 有公共的非最终方法 setChildren(List),它是微不足道的设置器,允许设置任何列表。

同时 TreeNodeChildren 扩展 List 覆盖每个添加或删除添加到每个人的项目的方法,调用私有 updateRowKeys。每次树结构更改时,updateRowKeys 都会重新计算整个子树的行键,并使用 DefaultTreeNode::setRowKey 存储计算的键。 (真是一团糟!)。这些行键的形式为:“0_1_1”,反映了层次结构。并且它们(以某种方式)用于渲染树。如果 rowkeys 的结构不同 p:tree 不能正确呈现节点标签(只是空标签)。

因此,要使 p:tree 与 DefaultTreeNode 的子类或实现 TreeNode 的类一起工作,您必须覆盖 getRowKey() 或使用 updateRowKeys 实现该算法。这是第二个没有记录的技巧。我更喜欢覆盖 getRowKey() 以支持 AbstractTreeNode 扩展 DefaultTreeNode 中挖掘的密钥结构“0_1_1”:

public abstract class AbstractTreeNode<T> extends DefaultTreeNode 

    @Override
    public abstract String getType();

    @Override
    public abstract AbstractTreeNode<T> getParent();

    @Override
    public abstract void setParent(TreeNode tn);

    @Override
    public abstract List<TreeNode> getChildren();

    public AbstractTreeNode()  super(); 

    @Override
    public T getData() 
        return (T)this;
    

    @Override
    public int getChildCount() 
        if (getChildren() == null) return 0;
        return getChildren().size();
    

    @Override
    public boolean isLeaf() 
        return (getChildren() == null) || getChildren().isEmpty();
    

    @Override
    public final String getRowKey() 
        String r;
        if (getParent() == null) r = "";
        else if (getParent().getParent() == null)
            r = ""+getParent().getChildren().indexOf(this);
        else 
            r = getParent().getRowKey() + "_" + getParent().getChildren().indexOf(this);
        
        return r;
    

这里的 getType()、getParent()、setParent() 和 getChildren() 是纯抽象的,以允许子类反映其自己的具体树结构,并且 getRowKey() 是最终的,因为它不能不同(?进一步挖;-)

可以这样使用:

@Entity
public class SomeTable extends AbstractTreeNode<SomeTable> implements Serializable 
...

对不起,我的英语很差。希望这将有助于某人避免长时间挖掘。

【讨论】:

以上是关于p:treenode 与 DefaultTreeNode 子类的奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章

二叉搜索树

leetcode——235. 二叉搜索树的最近公共祖先

算法和数据结构 相同的树

文巾解题 100. 相同的树

leetcode 100. Same Tree

LeetCode -- 100. Same Tree