Multi select in RichFaces trees

In the past few months I have been involved in a development project where we are using Hibernate, Seam and RichFaces. One of the requirements of our customer is to have a hierarchical data structure represented in a tree structure. We found that the RichFaces rich:tree component meets all the requirements we have. Well, all but one: rich:tree is single select only, not multi select. Or is it?

Some theory behind the rich:tree component

The RichFaces rich:tree component is a component that can display hierachical data in two ways. The first way is to display a org.richfaces.model.TreeNode with its children. The RichFaces API also provides a default implementation of the org.richfaces.model.TreeNode interface, which is the org.richfaces.model.TreeNodeImpl class.

The second way is to use a RichFaces rich:recursiveTreeNodesAdaptor to display a java.util.List or array of any kind of object, as long as it has some member that holds a java.util.List or array of child objects. Due to some heavy preprocessing of the data that is displayed in the tree, along with us feeling more comfortable with java.util.List we choose this approach in our project. In this article I’ll use the first approach to show that our solution also works in this case.

Building a simple rich:tree

To be able to display a rich:tree in a JSF page, you need few simple classes. The first class I used is called SelectionBean and it looks like this

import org.richfaces.event.NodeSelectedEvent;
import org.richfaces.model.TreeNode;
import org.richfaces.model.TreeNodeImpl;

public class SelectionBean {

    private TreeNode rootNode = new TreeNodeImpl();

    public SelectionBean() {
        TreeNodeImpl childNode = new TreeNodeImpl();
        childNode.setData("childNode");
        childNode.setParent(rootNode);
        rootNode.addChild("1", childNode);

        TreeNodeImpl childChildNode1 = new TreeNodeImpl();
        childChildNode1.setData("childChildNode1");
        childChildNode1.setParent(childNode);
        childNode.addChild("1.1", childChildNode1);

        TreeNodeImpl childChildNode2 = new TreeNodeImpl();
        childChildNode2.setData("childChildNode2");
        childChildNode2.setParent(childNode);
        childNode.addChild("1.2", childChildNode2);
    }

    public void processTreeNodeImplSelection(final NodeSelectedEvent event) {
        System.out.println("Node selected : " + event);
    }

    public TreeNode getRootNode() {
        return rootNode;
    }
}

It creates a simple TreeNode hierarchy that can be displayed in a tree with the following Facelets JSF page:

<?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:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:a4j="http://richfaces.org/a4j"
      xmlns:rich="http://richfaces.org/rich">
    <body>
        <h:form id="main">
            <a4j:outputPanel ajaxRendered="true">
                <rich:panel id="treePanel">
                    <f:facet name="header">Tree</f:facet>
                    <rich:tree id="tree" ajaxSubmitSelection="true" switchType="ajax" value="#{selectionBean.rootNode}" var="node">
                        <rich:treeNode>
                            <h:outputText value="#{node}"/>
                        </rich:treeNode>
                    </rich:tree>
                </rich:panel>
            </a4j:outputPanel>
        </h:form>
    </body>
</html>

The result looks like this:

Multi select in RichFaces trees 01 simple richfaces tree

Quite simple, like I said.

Multi select in the rich:tree

The SelectionBean class has been prepared to catch node selection events. If you modify the rich:tree line in the Facelets JSF page to read like this

<rich:tree id="tree" ajaxSubmitSelection="true" switchType="ajax"
    value="#{selectionBean.rootNode}" var="node"
    nodeSelectListener="#{selectionBean.processTreeNodeImplSelection}">

you should see selection events being registered in the log of your application server:

Node selected : org.richfaces.event.AjaxSelectedEvent

Not very helpful information, but at least we know that the node selection events are registered. Now, if you look at the taglib doc for rich:tree you’ll notice that there is no way to configure the tree to accept multiple selections. So let’s modify the SelectionBean to keep track of node selections itself.

We’ll need some Set to hold the selected tree nodes in. We could use a List, but that will create doubles if we click a node more than one time. Remember, rich:tree has no way of knowing if a node recently was selected or not. So everytime we click a node that rich:tree thinks not to be selected, it raises a selection event again! We only want to know which nodes are clicked and keep track of that. we don’t want to know how many times a node is selected. So therefore a Set will do nicely.

There’s one more thing to a rich:tree. Its backing UIComponent is a org.richfaces.component.html.HtmlTree and in the TreeModel of that tree, each node is uniquely identified by a RowKey Object. So, I’ll use a

private Map<Object, TreeNode> selectedNodes = new HashMap<Object, TreeNode>();

Now, everytime a node is selected I’ll add its TreeNode to the Map under the RowKey key. Assuming we have a global Map member as defined above, the processTreeNodeImplSelection method can now be modified to

    public void processNodeSelection(final NodeSelectedEvent event) {
        HtmlTree tree = (HtmlTree)event.getComponent();
        Object rowKey = tree.getRowKey();
        TreeNode selectedNode = tree.getModelTreeNode(rowKey);
        selectedNodes.put(rowKey, selectedNode);
        for (Object curRowKey : selectedNodes.keySet()) {
            System.out.println("Selected node : " + selectedNodes.get(curRowKey).getData());
        }
    }

If you click the three nodes in the tree in some random order, you’ll get this output:

Selected node : childChildNode1
Selected node : childNode
Selected node : childChildNode2

So, we are keeping track of all selected nodes!

Making the selected nodes visible in the tree

Now our bean knows that we have selected multiple nodes, but the tree still displays only one selected node at a time. Using e.g. FireBug it’s easy to determine the difference is CSS class between a selected node and a non-selected node. Using the default Blue skin for RichFaces, a non-selected node has style class

dr-tree-h-text rich-tree-node-text

while a selected node has style class

dr-tree-h-text rich-tree-node-text dr-tree-i-sel rich-tree-node-selected

The border around a selected node is there because of the dr-tree-i-sel style. We only need a way to make all selected nodes (that is, the ones that are stored in the Map in our bean) use that style. One way is to tell each TreeNode that is has been selected. But how can we do that? Well, for instance by introducing a class that holds both the text that will be displayed in the tree as well as a Boolean that holds the selection state of the node. Such a class could be like this

public class NodeData {
    private String nodeText;

    private Boolean selected = Boolean.FALSE;

    public NodeData(String nodeText) {
        this.nodeText = nodeText;
    }

    [getters and setters]
}

With this class we need to make a few changes to our SelectionBean. First of all, when building the node hierarchy we need to use the NodeData class instead of a simple String. This means we’ll have to modify the constructor method so it looks like this

    public SelectionBean() {
        TreeNodeImpl childNode = new TreeNodeImpl();
        childNode.setData(new NodeData("childNode"));
        childNode.setParent(rootNode);
        rootNode.addChild("1", childNode);
        TreeNodeImpl childChildNode1 = new TreeNodeImpl();
        childChildNode1.setData(new NodeData("childChildNode1"));
        childChildNode1.setParent(childNode);
        childNode.addChild("1.1", childChildNode1);
        TreeNodeImpl childChildNode2 = new TreeNodeImpl();
        childChildNode2.setData(new NodeData("childChildNode2"));
        childChildNode2.setParent(childNode);
        childNode.addChild("1.2", childChildNode2);
    }

Next, the processNodeSelection method needs to tell a node that it is selected by setting the selected Boolean in NodeData to true. The method becomes

    public void processNodeSelection(final NodeSelectedEvent event) {
        HtmlTree tree = (HtmlTree)event.getComponent();
        Object rowKey = tree.getRowKey();
        TreeNode selectedNode = tree.getModelTreeNode(rowKey);
        ((NodeData)selectedNode.getData()).setSelected(Boolean.TRUE);
        selectedNodes.put(rowKey, selectedNode);
        for (Object curRowKey : selectedNodes.keySet()) {
            System.out.println("Selected node : " + ((NodeData)selectedNodes.get(curRowKey).getData()).getNodeText());
        }
    }

Finally, we need to modify our Facelets JSF page in two ways. The first one is to make sure the h:outputText element displays the nodeText of the NodeData. The second modification is to have the rich:treeNode set it’s nodeClass accordingly to the selected NodeData Boolean. The Facelets JSF page lines look like this

<rich:treeNode nodeClass="#{node.selected?'dr-tree-i-sel':''}">
    <h:outputText value="#{node.nodeText}"/>
</rich:treeNode>

Now, if you reload the application in your browser, all of a sudden you can “select” multiple nodes in the tree.

Multi select in RichFaces trees 02 multi select in tree

Future enhancements

The above scenario isn’t ideal. First of all, now single selection of nodes doesn’t work anymore. To fix this, you may want to add a checkbox that toggles the selection state from single to multiple and back. Another issue is that accidentically selected nodes cannot be deselected anymore. The selection state checkbox may partially solve that, however. Once you select a node that you didn’t want to select, toggle the checkbox, select a single node, then toggle the checkbox again and start selecting multiple nodes once more. Another way would be to have another checkbox that allows you to deselect any selected node. Finally, users may want to hold a key, e.g. the CTRL key, and then start selecting multiple nodes. I haven’t got a clue how to do that, so if you know please drop me an email 🙂

Ideally the RichFaces rich:tree would have native multiple selection support. Perhaps this post will actually make that possible.

15 Comments

  1. Vikas Mishra December 13, 2011
  2. Angelo October 4, 2011
  3. Leandro August 5, 2011
  4. Andriy Pashkevych August 1, 2011
  5. Cagatay Civici January 11, 2010
  6. Sanjeewa November 21, 2009
  7. Sanjeewa November 21, 2009
  8. zhuzhiwei November 18, 2009
  9. Krishna November 2, 2009
  10. Enrico July 14, 2009
  11. cuixf April 27, 2009
  12. yukimi April 3, 2009
  13. new April 3, 2009
  14. ismail March 18, 2009
  15. Mihai February 12, 2009