Expand tree node all the way down in the ADF Faces Tree Component

We are still very much busy developing our ADF Faces application around the Tree Component. In previous articles we have discussed creating the tree in the first place, using various Tables as underlying data sources, adding intermediate nodes with node category labels, correctly displaying the node-as-folder vs node-without-children icon and generating the Tree based application using JHeadstart. In a next post I will discuss how to navigate the tree using keyboard keys instead of the mouse. This post addresses another tree-related issue: how to expand (and collapse) a node with all its off-spring, all the way down to the lowliest details.

Expand tree node all the way down in the ADF Faces Tree Component 

It turns out it is quite difficult to do this in a very precise way, but very easy in a more general, just as effective fashion.

I will assume a simple application with ADF Faces Tree Component. It can be generated using JHeadstart – as discussed in Building an ADF Faces Tree Component with mixed (DEPT and EMP) nodes in 5 minutes using JHeadstart 10.1.3 and   Generating an Advanced ADF Faces Tree based application with JHeadstart 10.1.3 – or built directly with drag & drop from the ADF Data Control palette – as illustrated in Creating Multi-Type Node Children and Child Node labels in ADF Faces Tree Component – or even without any ADF binding with only the ADF Faces components and simple POJO style managed beans.  

....
 

We assume that the tree is bound to a backing bean, the selection of a node is registered with a backing bean and we use a command button to initiate the ‘expand currently selected node’ operation.

After creating the application (Web Technology Template ADF BC and ADF Faces) and developing the Business Components from Tables (EMP and DEPT in this example), I have created a new JSF JSP page, dragged the DeptView collection as Tree from the Data Control Palette to the JSF page and created the tree model that links in EmpView for the detail nodes.

Now it is time for a few simple steps:

  1. bind the tree to a backing bean
  2. make the nodes clickable and tell the backing bean which node was selected
  3. highlight the currently selected node by linking the tree’s focus to the backing bean
  4. add a command button (++) that initiates the expansion of the currently selected node
  5. link the button’s action to the backing bean 
  6. add a command button (–) that initiates the collapse of the currently selected node
  7. link the buttons action to the backing bean

1. Bind the tree to the backing bean:

<af:tree value="#{bindings.DeptView1.treeModel}" var="node"
binding="#{MyTreeBean.tree}">
<f:facet name="nodeStamp">
<af:outputText value="#{node}"/>
</f:facet>
</af:tree>

The tree component is bound to the tree property in the backing bean MyTreeBean. This bean is auto-created when this binding was made by the JDeveloper IDE. We will inspect the code shortly.

2. Make the nodes clickable and tell the backing bean which node was selected

To make the nodes clickable/selectable , we change their definition from outputText to commandLink:

<af:tree value="#{bindings.DeptView1.treeModel}" var="node"
binding="#{MyTreeBean.tree}">
<f:facet name="nodeStamp">
<af:commandLink text="#{node}">
<af:setActionListener from="#{MyTreeBean.tree.rowKey}"
to="#{MyTreeBean.focusRowKey}"/>
</af:commandLink>
</f:facet>
</af:tree> 

I have added the focusRowKey property to the TreeBean – with accessors. When we run the application as it sits right now, we can click on the nodes of the tree – and see nothing happen. Only if you run in Debug Mode and create a breakpoint in the setFocusRowKey() method can you see that the method is invoked when the node is selected. 

3. highlight the currently selected node by linking the tree’s focus to the backing bean

The tree will display the selected node in a highlighted fashion, if it knows which node is the selected one. This is specified through the tree’s focusRowKey attribute. We can associate this attribute with the focusRowKey property of our tree bean:

<af:tree value="#{bindings.DeptView1.treeModel}" var="node"
focusRowKey="#{MyTreeBean.focusRowKey}"
binding="#{MyTreeBean.tree}">
<f:facet name="nodeStamp">
<af:commandLink text="#{node}">
<af:setActionListener from="#{MyTreeBean.tree.rowKey}"
to="#{MyTreeBean.focusRowKey}"/>
</af:commandLink>
</f:facet>
</af:tree> 

Now when we run the application, we will see the selected node as the highlighted one. 

4. add a command button (++) that initiates the expansion of the currently selected node

<h:form>
<h:panelGrid columns="2">
<af:panelGroup>
<af:commandButton immediate="true"
actionListener="#{MyTreeBean.expandCurrent}"
text="++">
</af:commandButton>
</af:panelGroup>
<af:panelGroup>
<af:tree value="#{bindings.DeptView1.treeModel}" var="node"
focusRowKey="#{MyTreeBean.focusRowKey}" 

Our button invokes the expandCurrent() method – see next section.

5. link the button’s action to the backing bean 

 

Here is the heart of this article. The ADF Tree Component has a TreeState – a collection that describes which nodes in the tree are expanded and which ones are not. The TreeState collection contains List elements. Each List Elements specifies a node, by specifying a tree path. The treepath of a node is something like [0,0,1] for the second child of the first child of the first root-node or [0] for the root-node itself or [4,2,1,5] for the 6th child of the 2nd child of the 3rd child of the 5th root-node.

We can programmatically expand nodes by adding them to the TreeState of the tree.  

public void expandCurrent(ActionEvent event) {
List currentFocusRowKey = new ArrayList();
currentFocusRowKey.addAll(getFocusRowKey());
int currentLevel = currentFocusRowKey.size();
getTree().getTreeState().getKeySet().add(currentFocusRowKey);
expandNode(currentLevel, currentFocusRowKey, 10); // 10 levels deep
}

private void expandNode(int currentLevel, List currentFocusRowKey,
int numberOfLevels) {
if (numberOfLevels > 0) {
currentLevel++;
f or (int i = 0; i < 10; i++) { // no more than 10 children per node
currentFocusRowKey.add(Integer.toString(i));
getTree().getTreeState().getKeySet().add(currentFocusRowKey);
expandNode(currentLevel, currentFocusRowKey, --numberOfLevels);
currentFocusRowKey.remove(currentLevel - 1);
} //for
currentLevel--;
}
}
 

Note: when using the ADF Faces Tree component together with the ADF Binding Tree element (FacesCtrlHierBinding), it turns out to be not so very simple to determine which nodes have children and how many of them. It is far easier to just add a lot of potential tree paths to the tree state of the tree – including many treepaths that do not refer to real nodes. Since the tree state merely serves the tree renderer to see whether or not a node should be displayed as expanded – and whether its children must be displayed – it does not hurt to have these non-existent tree paths in there.

6. add a command button (–) that initiates the collapse of the currently selected node

From where we are standing now, adding the Collapse Current Node functionality is a piece of cake. First add the button:

<h:panelGrid columns="2">
<af:panelGroup layout="vertical">
<af:commandButton immediate="true"
actionListener="#{MyTreeBean.expandCurrent}"
text="++">
</af:commandButton>
<af:objectSpacer height="10" />
<af:commandButton immediate="true"
actionListener="#{MyTreeBean.collapseCurrent}"
text="--">
</af:commandButton>
</af:panelGroup>
<af:panelGroup>
 

and then

7. Link the button’s action to the backing bean

We need to implement the method collapseCurrent in our Tree Bean:

    public void collapseCurrent(ActionEvent event) {
List currentFocusRowKey = new ArrayList();
currentFocusRowKey.addAll(getFocusRowKey());
getTree().getTreeState().getKeySet().remove(currentFocusRowKey);
Iterator keys = getTree().getTreeState().getKeySet().iterator();
while (keys.hasNext()) {
List rowKey = (List)keys.next();
if (listStartsWith(rowKey, currentFocusRowKey)) {
getTree().getTreeState().getKeySet().remove(rowKey);
}
}
}

private boolean listStartsWith(List check, List against) {
if (check.size()> against.size()) {
for (int i = 0;i < Math.min(check.size(),against.size());i++ ) {
if (!((String)check.get(i)).equalsIgnoreCase((String)against.get(i)))
return false;
}// for
return true;
}// if
return false;
}
 

The essence here is that the tree-paths of the current node and all of its offspring are removed from the TreeState collection of the tree. So taking the current node’s tree path which is the same as the current focusRowKey and removing it from the TreeState is enough to close or collapse the current node. However, if we re-open the node, all of its children are potentially still expanded. So collapseCurrent needs to remove all entries in the TreeState that begin with focusRowKey – since all those entries are offspring of the current node.

JHeadstart and generating the Tree Application

Much of the inspiration for this approach comes from the JHeadstart team. The added value over JHeadstart presented in this article is the ability to expand and collapse a specific node – rather than the entire tree, which is a built-in feature of generated JHeadstart trees. 

If you are using JHeadstart, all you have to do is extend the JHeadstart TreeBean and create the expandCurrent() and collapseCurrent() methods with their helpers. And of course add the buttons to the generated pages.

Resources

Download the JDeveloper 10.1.3 Application with the HRM Tree implementation: TreeExpandCollapse.zip (4Mb)

One Response

  1. Ric Smith August 30, 2006