Multi select in RichFaces trees

15

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:

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[source=org.richfaces.component.html.HtmlTree@19eaa86]

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.

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.

Share.

About Author

15 Comments

  1. Hi Andriy, i’m really interested in your code.
    it would be great if you post the full example!
    Thank you.

  2. Hi Andriy, i’m really interested in your code.
    it would be great if you post the full example!
    Thank you.

  3. Andriy Pashkevych on

    Hi!
    I’ve implemented the same feature with rich:tree but in a completely different way.
    To be able to select multiple I’ve used checkboxes inside each row to display the node’s “selected” property, and processed that data on submit.
    If  someone needs an example I can post a code here.
     
    Regards

  4. Dear cuixf and Krishna,

    Change the following property in your jboss-service.xml file.

    File:
    jboss-4.2.2.GA\server\default\deploy\jboss-web.deployer\META-INF\jboss-service.xml

    Should change to:

    true

    Regards

  5. thanks for your article, sir
    you said:”you should see selection events being registered in the log of your application server:”, how could i register the selection event in the example?

    help me please thank u

  6. I am getting the same InvocationTargetException, as mentioned by cuixf and other above.
    Any solution to that?

  7. Hello,
    Thank you for the tutorial.
    Unfortunetly i still couldn’t make the first simple tree on the tutorial.
    I think i need you help.

    First i want to tell you that i made a Seam Web Project with JBoss AS 4.2 Runtime.
    When you made those simple tree, what kind of project did u have? A Dynamic Web Project or Seam Web Project?

    And in my eclipse, there is no errror as i copied your jsf Page.

    But on the browser i didn’t see anything. It’s only a white page.

    Could you help me please?

    Thank you

  8. The WAR’s project is no problem,but my EAR’s project see InvocationTargetException warning.

    ClassLoader ???

    _______________________________________________
    yukimi Says:
    April 3rd, 2009 at 12:01 pm
    Hi, thank you for this example. It’s great. However, I have problem with HtmlTree. Using your source code, whenever I click on the node, it shows
    18:06:10,354 WARN [lifecycle] /admin/category.xhtml @28,79 nodeSelectListener=”#{CategoryManager.processNodeSelection}”: java.lang.reflect.InvocationTargetException
    If I removed HtmlTree, I don’t see InvocationTargetException warning. What’s wrong?

    Please help.
    Thanks.

  9. Hi, thank you for this example. It’s great. However, I have problem with HtmlTree. Using your source code, whenever I click on the node, it shows
    18:06:10,354 WARN [lifecycle] /admin/category.xhtml @28,79 nodeSelectListener=”#{CategoryManager.processNodeSelection}”: java.lang.reflect.InvocationTargetException
    If I removed HtmlTree, I don’t see InvocationTargetException warning. What’s wrong?

    Please help.
    Thanks.

  10. hi ı have prolem with HtmlTree ı couldnt find org.richfaces.component.html.HtmlTree library

    please help me how to parse this

    thanks

  11. Dear Sir,

    Many thanks for this type of post. Its self-explanatory. Currently I am working on rich:tree with checkbox. If you have any idea or progress regarding checkbox tree, please share with us.

    Thank you
    Ismail
    =====

  12. Thanks for the post, it helped a lot. I still have a problem, though. I can select multiple nodes but I do not see them selected only after I refresh the page. Do you have the same problem with your implementation? If you know how to solve it, please respond.