Monday, November 25, 2013

ADF Tree

In this post I will try to show how to implement the following requirements:
  1. database-driven tree.
  2. either render the node as a text output or a link depending on the values of the database record.
  3. for the disclosed node, if it has a child node and the child node is a leaf, hide the disclose icon next to it.
  4. once you click on a node in the tree, the path to the node will be printed in the screen.
  5. if you click on a node, it will collapse if it was expanded and vice versa.

how to implement a database-driven tree:
Database part:
you can use the following table or some other table similar to it:
TreeMenu:
id : number,primary key
parent_id : number, foreign key (self reference to the same table and uses id the source), id of the parent node
Adesc: varchar2(xxx), the text to be displayed for the node in the tree.
Link: varchar2(xxx), the link to be used for leaf nodes.

ADF model project part:
after that use  create business components from tables wizard of JDeveloper, you will get:
one entity object, TreeMenu.
one associoation, based on the foreign key constraint.
one view object:TreeMenu
one View Link , based on the association.

I suggest you change the names of the successor for the association and the view link to avoid confusion in the future.

next, open the TreeMenu view object in the editor in the overview tab, and go to the query  part,and define a new View Criteria, let's name it isRoot.
and use the following condition : parent_id is null, we will use this view criteria to differentiate between root nodes and others.

next, open the application module in the editor in the overview tab and go to the Data Model and add one TreeMenu view object to the data model and name the instance: RootNodes, then choose the RootNodes instance in the data model and click the Edit button on the upper left part of the data model and choose the isRoot viewCriteria to be applied to this instance, this will insure that only root nodes will be fetched for this instance.



after that, add a child node to the RootNodes instance using the View link, and name the instance ChildrenNodes.

with this the model project is ready.

ADF view controller:
create a new page, expand the data controls palette of the JDeveloper and you should see your application module data control with RootNodes instance with ChildrenNodes listed as a detail.
click and drag the RootNodes to the page and choose to render it as an ADF tree.
add a sub node to the tree, and select the attributes you want to display


after that edit the source of the page to be similar to:
<af:tree value="#{bindings.RootNodes.treeModel}" var="node"
               selectionListener="#{bindings.RootNodes.treeModel.makeCurrent}"
               rowSelection="single" id="t1">
        <f:facet name="nodeStamp">
          <af:panelGroupLayout id="pgl1">
            <af:outputText value="#{node.Edesc}" id="ot1"
                           rendered="#{empty node.Link}"/>
            <af:goLink text="#{node.Edesc}:#{node.Link}" id="gl1"
                       destination="#{node.Link}"
                       rendered="#{!empty node.Link}" targetFrame="_blank"/>
          </af:panelGroupLayout>
        </f:facet>
      </af:tree>

with this you should get an output similar to:
as you can see from the above image, initially all nodes have a disclose icon next to them that you can click to disclose the node and show child records. but I find it annoying that each node have this icon, even if this node is a leaf and has no children but will disappear once you click on it.

I searched around the internet to see if it's possible to hide this icon through some config, but did not find any declarative approach so I had to use the programmatic solution which I will describe now:
  • on the tree component of the page define a rowDisclosureListener
  • in the rowDisclosureListener add the following code:

    public void disclosureListener(RowDisclosureEvent rowDisclosureEvent) {
       
        RichTree tree=(RichTree)rowDisclosureEvent.getSource();
        RowKeySet rks=rowDisclosureEvent.getAddedSet();
        if(rks!=null&&rks.size()>0){
            Iterator iter=rks.iterator();

            while(iter.hasNext()){
                Object rowKey=iter.next();
                tree.setRowKey(rowKey);
                JUCtrlHierNodeBinding rowData=(JUCtrlHierNodeBinding)tree.getRowData();
                if(rowData!=null&&rowData.getChildren()!=null){ // Iterate through the children of the expanded node and check if they have children
                    for(Object child:rowData.getChildren()){
                        JUCtrlHierNodeBinding childNode=(JUCtrlHierNodeBinding)child;
                        if(childNode.getChildren()==null||childNode.getChildren().size()==0){ // Child node is a leaf.  Add it to the disclosed rows, to that the ADF tree will not display a disclosure icon
                            tree.getDisclosedRowKeys().add(childNode.getKeyPath());
                        }
                    }
                }
            }
           
        }
    }

rerun the application, now child nodes won't have the disclose icon next to them from the start.

when I was looking for a way to hide the disclose icon, I came across this requirement:
when I click on a node in the tree I want to print the path from the root node to the click node on the screen.

I will implement this requirement by doing:

  • on the tree define a selectionListener.
  • in the selectionListener add the following code:

        RichTree tree1=(RichTree)selectionEvent.getSource();
        JUCtrlHierNodeBinding start = null;
        tree1.setRowKey(selectionEvent.getAddedSet().iterator().next());
        start = (JUCtrlHierNodeBinding)tree1.getRowData();
         
        JUCtrlHierNodeBinding parent=start.getParent();
        ZamerTreeMenuViewRowImpl  r = (ZamerTreeMenuViewRowImpl)start.getRow();
        path+="/"+r.getAdesc();
        while(parent!=null){
            r = (ZamerTreeMenuViewRowImpl)parent.getRow();
            if(r==null){
                break;
            }
            path="/"+r.getAdesc()+path;
            parent=parent.getParent();
        }
  • in the bean define a path variable and create a getter for it.
  • modify the page, by adding an output text to display the path,surround the output text with panel group layout and set partial triggers to rerender on tree changes. it will look similar to:
 <af:panelGroupLayout id="pgl2" layout="vertical" partialTriggers="t1">
        <af:outputText value="#{Test2.path}" id="ot2"/>
        <af:tree value="#{bindings.RootNodes.treeModel}" var="node"
                 selectionListener="#{Test2.selectionListener}"
                 rowSelection="single" id="t1"
                 rowDisclosureListener="#{Test2.disclosureListener}">
          <f:facet name="nodeStamp">
            <af:panelGroupLayout id="pgl1">
              <af:outputText value="#{node.Edesc}" id="ot1"
                             rendered="#{empty node.Link}"/>
              <af:goLink text="#{node.Edesc}:#{node.Link}" id="gl1"
                         destination="#{node.Link}"
                         rendered="#{!empty node.Link}" targetFrame="_blank"/>
            </af:panelGroupLayout>
          </f:facet>
        </af:tree>
      </af:panelGroupLayout>
the output will be similar:

I remember that in the earliest project I worked on, we had to implement a tree in a page where we had to expand/collapse the tree when we click on the tree item itself. to do this in ADF do the following :
  • change the page to look like:

<af:panelGroupLayout id="pgl2" layout="vertical" partialTriggers="t1">
        <af:outputText value="#{Test2.path}" id="ot2"/>
        <af:tree value="#{bindings.RootNodes.treeModel}" var="node"
                 selectionListener="#{Test2.selectionListener}"
                 rowSelection="single" id="t1"
                 rowDisclosureListener="#{Test2.disclosureListener}"
                 binding="#{Test2.treeMenu}">
          <f:facet name="nodeStamp">
            <af:panelGroupLayout id="pgl1">
              <af:commandLink text="#{node.Edesc}" id="cl1"
                              rendered="#{empty node.Link}"
                              actionListener="#{Test2.toggleNode}"/>
              <af:goLink text="#{node.Edesc}:#{node.Link}" id="gl1"
                         destination="#{node.Link}"
                         rendered="#{!empty node.Link}" targetFrame="_blank"/>
            </af:panelGroupLayout>
          </f:facet>
        </af:tree>
      </af:panelGroupLayout>
  • modify the code in the bean to:
package testing;

import java.util.Iterator;


import javax.faces.event.ActionEvent;

import model.businessObjects.view.ZamerTreeMenuViewRowImpl;

import oracle.adf.view.rich.component.rich.data.RichTree;


import oracle.jbo.uicli.binding.JUCtrlHierNodeBinding;

import org.apache.myfaces.trinidad.event.RowDisclosureEvent;
import org.apache.myfaces.trinidad.event.SelectionEvent;
import org.apache.myfaces.trinidad.model.RowKeySet;

public class Test2 {
    private RichTree treeMenu;

    public Test2() {
    }
    private String path = "";

    public void disclosureListener(RowDisclosureEvent rowDisclosureEvent) {
        RichTree tree = (RichTree)rowDisclosureEvent.getSource();
        RowKeySet rks = rowDisclosureEvent.getAddedSet();
        if (rks != null && rks.size() > 0) {
            Iterator iter = rks.iterator();

            while (iter.hasNext()) {
                Object rowKey = iter.next();
                tree.setRowKey(rowKey);
                JUCtrlHierNodeBinding rowData = (JUCtrlHierNodeBinding)tree.getRowData();
                if (rowData != null && rowData.getChildren() != null) { // Iterate through the children of the expanded node and check if they have children
                    for (Object child : rowData.getChildren()) {
                        JUCtrlHierNodeBinding childNode = (JUCtrlHierNodeBinding)child;
                        if (childNode.getChildren() == null || childNode.getChildren().size() == 0) { // Child node is a leaf.  Add it to the disclosed rows, to that the ADF tree will not display a disclosure icon
                            tree.getDisclosedRowKeys().add(childNode.getKeyPath());
                        }
                    }
                }
            }

        }
    }


    public void setPath(String path) {
        this.path = path;
    }

    public String getPath() {
        return path;
    }

    public void selectionListener(SelectionEvent selectionEvent) {
        //#{bindings.RootNodes.treeModel.makeCurrent}
        RichTree tree1 = (RichTree)selectionEvent.getSource();
        JUCtrlHierNodeBinding start = null;
        tree1.setRowKey(selectionEvent.getAddedSet().iterator().next());
        start = (JUCtrlHierNodeBinding)tree1.getRowData();
        updatePath(start);
    }

    private void updatePath(JUCtrlHierNodeBinding start) {
        String path = "";
        JUCtrlHierNodeBinding parent = start.getParent();
        ZamerTreeMenuViewRowImpl r = (ZamerTreeMenuViewRowImpl)start.getRow();
        path += "/" + r.getAdesc();
        while (parent != null) {
            r = (ZamerTreeMenuViewRowImpl)parent.getRow();
            if (r == null) {
                break;
            }
            path = "/" + r.getAdesc() + path;
            parent = parent.getParent();
        }
        this.path = path;
    }


    public void toggleNode(ActionEvent actionEvent) {

        RichTree tree1 = getTreeMenu();
        JUCtrlHierNodeBinding start = null;
        start = (JUCtrlHierNodeBinding)tree1.getRowData();
        org.apache.myfaces.trinidad.model.RowKeySet newRowSet = tree1.getDisclosedRowKeys().clone();
        boolean added = newRowSet.add(start.getKeyPath());
        if (!added && start.getChildren() != null && start.getChildren().size() > 0) {
            newRowSet.remove(start.getKeyPath());
        }
        new RowDisclosureEvent(tree1.getDisclosedRowKeys(), newRowSet, tree1).queue();
        updatePath(start);


    }

    public void setTreeMenu(RichTree treeMenu) {
        this.treeMenu = treeMenu;
    }

    public RichTree getTreeMenu() {
        return treeMenu;
    }
}


now, upon clicking on an item in the tree( not the disclose icon) the tree will either expand or collapse based on the item you clicked.