Building a Dynamic Tree with JBoss RichFaces and Spring Web Flow

January 19th, 2010 by John Turner Leave a reply »

Recently I was involved in a project that required a user interface that would facilitate the manipulation of a hierarchical data structure.  I was already using JBoss RichFaces so it made perfect sense to utilise the tree and associated components (i.e. recursiveTreeNodesAdaptor and treeNode).  The project was also utilising the Spring Framework and Spring Web Flow and these were also incorporated into the eventual solution.

To start with, as any good developer does, I searched for existing solutions to the problem and while I found some useful information I did not find exactly what I was looking for.  With this in mind, when I eventually finished the development of the user interface I decided I would share my implementation and hopefully you will find what follows useful.

To allow you to follow along, I have made the source (maven project) available as a download.  This way, I do not have to worry about explaining everything in minute detail.

For a preview, you can always build the source, deploy to a server and navigate to http://[server]:[port]/dynamic-tree/spring/flow-main replacing the [server] and [port] with appropriate values.

The Data Structure

For the purpose of demonstrating my implementation, I am going to use a simple organisational hierarchy (as discussed in Martin Fowlers book ‘Analysis Patterns’) as illustrated below:


Data Structure

Figure 1 : Data Structure




This is a very basic hierarchical data structure but it suffices for my purpose.  The implementation of this class is also very basic and requires little explanation.  To keep things simple, I have not considered the persistence mechanism that was employed.

package prototype.dynamictree.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class Organisation implements Serializable {

  private static final long serialVersionUID = -2023156931701914562L;

  private List<Organisation> children;

  private String description;

  private String name;

  private Organisation parent;

  public void addChild(Organisation child) {
    if (child.getParent() == null || !child.getParent().equals(this)) {
      child.setParent(this);
    }

    if (children == null) {
      children = new ArrayList<Organisation>();
    }

    if (!children.contains(child)) {
      children.add(child);
    }
  }

  public void removeChild(Organisation child) {
    if (child.getParent() != null && child.getParent().equals(this)) {
      child.setParent(null);
    }

    if (children != null) {
      children.remove(child);
    }
  }

  public List<Organisation> getChildren() {
    if (children == null) {
      children = new ArrayList<Organisation>();
    }

    return Collections.unmodifiableList(children);
  }

  public String getDescription() {
    return description;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Organisation getParent() {
    return parent;
  }

  public void setParent(Organisation parent) {
    this.parent = parent;
  }
}

Listing 1 : Organisation.java



Strictly speaking, a Set would have been a more appropriate data structure for the Organisation’s children but I encountered strange behaviour by the tree component if Set was used as opposed to List.

Displaying the Hierarchical Data

As already mentioned, I used Spring Web Flow to manage the UI conversations and binding with the entity model.  The first step in the flow creates the root Organisation and sets a default name (Joe Bloggs Inc).  The second step is a view step that renders the hierarchical data using the tree component.

<rich:page
  xmlns:a4j="http://richfaces.org/a4j"
  xmlns:rich="http://richfaces.org/rich"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core"
  markupType="xhtml" sidebarWidth="100" width="400"
  pageTitle="Dynamic Tree User Interface">

  <f:view>
    <h:form>
      <rich:panel id="dynamicTreePanel" header="Dynamic Tree User Interface">
        <rich:tree icon="/image/node.gif" iconLeaf="/image/node.gif">
                    
          <rich:recursiveTreeNodesAdaptor roots="#{rootOrganisation}" var="_rootOrganisation">
            <rich:treeNode>
              <h:outputText value="#{_rootOrganisation.name}"/>
            </rich:treeNode>
                        
            <rich:recursiveTreeNodesAdaptor roots="#{_rootOrganisation.children}"
                var="_childOrganisation" nodes="#{_childOrganisation.children}">
                            
              <rich:treeNode>
                <h:outputText value="#{_childOrganisation.name}"/>
              </rich:treeNode>
            </rich:recursiveTreeNodesAdaptor>
          </rich:recursiveTreeNodesAdaptor>
        </rich:tree>
      </rich:panel>
    </h:form>
  </f:view>
</rich:page>

Listing 2 : dynamicTree.xhtml



The reason I have used a nested recursiveTreeNodesAdaptor component is that later in the process, I will be associating a different contextMenu component with the root node than other nodes.

Upon executing the flow the following is rendered.


Figure 2: rendered View

Figure 2 : Rendered View




Creating the Node Selection Listener

In order to manipulate the tree structure, we will need to be able to determine the currently selected node.  This is achieved by creating a class to listen for the node selection event and configuring this class to receive the event.

The code for the class is straight forward and the listing is as follows:

package prototype.dynamictree.bean;

import java.io.Serializable;

import org.richfaces.component.html.HtmlTree;
import org.richfaces.event.NodeSelectedEvent;

public class DynamicTree implements Serializable {

  private static final long serialVersionUID = 4935795680279034480L;

  private Object selectedNodeData;

  public void processNodeSelection(NodeSelectedEvent event) {
    HtmlTree tree = ((HtmlTree) event.getComponent());

    selectedNodeData = tree.getRowData();
  }

  public Object getSelectedNodeData() {
    return selectedNodeData;
  }
}

Listing 3 : DynamicTree.java



The method getSelectedNodeData will return the Organisation associated with the selected node.

To configure the class to receive the node selection event (via ajax) the tree component attributes need to be amended as follows:

<rich:page
  xmlns:a4j="http://richfaces.org/a4j"
  xmlns:rich="http://richfaces.org/rich"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core"
  markupType="xhtml" sidebarWidth="100" width="400"
  pageTitle="Dynamic Tree User Interface">

  <f:view>
    <h:form>
      <rich:panel id="dynamicTreePanel" header="Dynamic Tree User Interface">
        <rich:tree ajaxSubmitSelection="true"
            ajaxSingle="true"
            nodeSelectListener="#{dynamicTree.processNodeSelection}"
            rightClickSelection="true"
            icon="/image/node.gif" iconLeaf="/image/node.gif">
                    
          <rich:recursiveTreeNodesAdaptor roots="#{rootOrganisation}" var="_rootOrganisation">
            <rich:treeNode>
              <h:outputText value="#{_rootOrganisation.name}"/>
            </rich:treeNode>
                        
            <rich:recursiveTreeNodesAdaptor roots="#{_rootOrganisation.children}"
                var="_childOrganisation" nodes="#{_childOrganisation.children}">
                            
              <rich:treeNode>
                <h:outputText value="#{_childOrganisation.name}"/>
              </rich:treeNode>
            </rich:recursiveTreeNodesAdaptor>
          </rich:recursiveTreeNodesAdaptor>
        </rich:tree>
      </rich:panel>
    </h:form>
  </f:view>
</rich:page>

Listing 4 : dynamicTree.xhtml



Adding Support for Creating Child Nodes

Now that we are being notified of the node selection event we will be able to add child nodes to the tree component. This will achieved by adding 2 context menu components (one for the root node and one for all others) and a modal dialog.

<rich:page
  xmlns:a4j="http://richfaces.org/a4j"
  xmlns:rich="http://richfaces.org/rich"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core"
  markupType="xhtml" sidebarWidth="100" width="400"
  pageTitle="Dynamic Tree User Interface">

  <a4j:loadStyle src="/css/style.css"/>

  <f:view>
    <h:form>
      <rich:panel id="dynamicTreePanel" header="Dynamic Tree User Interface">
        <rich:tree ajaxSubmitSelection="true"
            ajaxSingle="true"
            nodeSelectListener="#{dynamicTree.processNodeSelection}"
            rightClickSelection="true"
            icon="/image/node.gif" iconLeaf="/image/node.gif">
                    
          <rich:recursiveTreeNodesAdaptor roots="#{rootOrganisation}" var="_rootOrganisation">
            <rich:treeNode>
              <h:outputText value="#{_rootOrganisation.name}"/>
              
              <rich:componentControl disableDefault="true" event="oncontextmenu"
                  for="rootContextMenu" operation="show"/>
            </rich:treeNode>
                        
            <rich:recursiveTreeNodesAdaptor roots="#{_rootOrganisation.children}"
                var="_childOrganisation" nodes="#{_childOrganisation.children}">
                            
              <rich:treeNode>
                <h:outputText value="#{_childOrganisation.name}"/>
                
                <rich:componentControl disableDefault="true" event="oncontextmenu"
                    for="childContextMenu" operation="show"/>
              </rich:treeNode>
            </rich:recursiveTreeNodesAdaptor>
          </rich:recursiveTreeNodesAdaptor>
        </rich:tree>
      </rich:panel>
      
            
      <!-- context menu's -->
      <rich:contextMenu id="rootContextMenu" attached="false" submitMode="ajax">
        <rich:menuItem>
          <h:outputText value="Add Child"/>
                    
          <a4j:support event="onclick" action="addChild"
              reRender="dynamicTreePanel, addChildModalPanel"
              oncomplete="Richfaces.showModalPanel('addChildModalPanel')"/>
        </rich:menuItem>
      </rich:contextMenu>
            
      <rich:contextMenu id="childContextMenu" attached="false" submitMode="ajax">
        <rich:menuItem>
          <h:outputText value="Add Child"/>

          <a4j:support event="onclick" action="addChild"
              reRender="dynamicTreePanel, addChildModalPanel"
              oncomplete="Richfaces.showModalPanel('addChildModalPanel')"/>
        </rich:menuItem>
      </rich:contextMenu>
            
      <!-- modal panels -->
      <rich:modalPanel id="addChildModalPanel"
          resizeable="false" height="200" width="330">

        <f:facet name="header">
          <h:outputText value="Add Organisation"/>
        </f:facet>
                
        <h:panelGrid columns="2" rendered="#{addChildModalPanelRendered}"
            columnClasses="verticalAlignTop">
          
          <h:outputText value="Name:"/>
          <h:inputText value="#{addChildOrganisation.name}"/>
                    
          <h:outputLabel value="Description:"/>
          <h:inputTextarea value="#{addChildOrganisation.description}"
              rows="5" cols="30"/>
        </h:panelGrid>
                
        <h:panelGroup style="display:block; text-align:center">
          <a4j:commandButton action="addChildModalPanelConfirm" value="Ok"
              onclick="Richfaces.hideModalPanel('addChildModalPanel')"
              reRender="dynamicTreePanel, addChildModalPanel"
              styleClass="commandButton"/>
    
          <a4j:commandButton action="addChildModalPanelCancel" value="Cancel"
              onclick="Richfaces.hideModalPanel('addChildModalPanel')"
              reRender="dynamicTreePanel, addChildModalPanel"
              styleClass="commandButton"/>
        </h:panelGroup>
      </rich:modalPanel>
    </h:form>
  </f:view>
</rich:page>

Listing 5 : dynamicTree.xhtml



Figure3 : Context Menu

Figure3 : Context Menu




As you can see, when the user right clicks on a tree node, the appropriate context menu will be displayed. If the user then clicks on ‘Add Child’ the addChild flow transition is invoked (via ajax) which creates a child Organisation object. On return from this ajax call the dynamicTreePanel and addChildModalPanel will be re-rendered before displaying the modal panel.


Figure 4 : Modal Panel

Figure 4 : Modal Panel




When the user clicks on the Ok button, the modal dialog is hidden and the addChildModalPanelConfirm flow transition is invoked. This transition adds the child Organisation to its parent and again on return from this ajax call the dynamicTreePanel and addChildModalPanel will be re-rendered.


When the user clicks on the Cancel button, the modal dialog is hidden and the addChildModalPanelCancel flow transition is invoked. On return from this ajax call the dynamicTreePanel and addChildModalPanel will be re-rendered.


Figure 5 : Tree with child nodes.

Figure 5 : Tree with child nodes.




I’m sure the wide awake among you have also noticed the addChildModalPanelRendered property.  This property is used to make sure the components that refer to a child Organisation are not rendered if no child organisation exists in the current conversation (i.e. when we are not actually adding a child organisation).


Adding Support for Editing Node Data

Now that we are able to add a child node, it is relatively straight forward to add support for editing a node. To achieve this, we will have to add 2 menu items to the already existing context menus and add another modal dialog.

<rich:page
  xmlns:a4j="http://richfaces.org/a4j"
  xmlns:rich="http://richfaces.org/rich"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core"
  markupType="xhtml" sidebarWidth="100" width="400"
  pageTitle="Dynamic Tree User Interface">

  <a4j:loadStyle src="/css/style.css"/>

  <f:view>
    <h:form>
      <rich:panel id="dynamicTreePanel" header="Dynamic Tree User Interface">
        <rich:tree ajaxSubmitSelection="true"
            ajaxSingle="true"
            nodeSelectListener="#{dynamicTree.processNodeSelection}"
            rightClickSelection="true"
            icon="/image/node.gif" iconLeaf="/image/node.gif">
                    
          <rich:recursiveTreeNodesAdaptor roots="#{rootOrganisation}" var="_rootOrganisation">
            <rich:treeNode>
              <h:outputText value="#{_rootOrganisation.name}"/>
              
              <rich:componentControl disableDefault="true" event="oncontextmenu"
                  for="rootContextMenu" operation="show"/>
            </rich:treeNode>
                        
            <rich:recursiveTreeNodesAdaptor roots="#{_rootOrganisation.children}"
                var="_childOrganisation" nodes="#{_childOrganisation.children}">
                            
              <rich:treeNode>
                <h:outputText value="#{_childOrganisation.name}"/>
                
                <rich:componentControl disableDefault="true" event="oncontextmenu"
                    for="childContextMenu" operation="show"/>
              </rich:treeNode>
            </rich:recursiveTreeNodesAdaptor>
          </rich:recursiveTreeNodesAdaptor>
        </rich:tree>
      </rich:panel>
      
            
      <!-- context menu's -->
      <rich:contextMenu id="rootContextMenu" attached="false" submitMode="ajax">
        <rich:menuItem>
          <h:outputText value="Add Child"/>
                    
          <a4j:support event="onclick" action="addChild"
              reRender="dynamicTreePanel, addChildModalPanel"
              oncomplete="Richfaces.showModalPanel('addChildModalPanel')"/>
        </rich:menuItem>

        <rich:menuSeparator/>

        <rich:menuItem>
          <h:outputText value="Edit"/>

          <a4j:support event="onclick" action="editChild"
              reRender="dynamicTreePanel, editChildModalPanel"
              oncomplete="Richfaces.showModalPanel('editChildModalPanel')"/>
        </rich:menuItem>
      </rich:contextMenu>
            
      <rich:contextMenu id="childContextMenu" attached="false" submitMode="ajax">
        <rich:menuItem>
          <h:outputText value="Add Child"/>

          <a4j:support event="onclick" action="addChild"
              reRender="dynamicTreePanel, addChildModalPanel"
              oncomplete="Richfaces.showModalPanel('addChildModalPanel')"/>
        </rich:menuItem>
        
        <rich:menuSeparator/>

        <rich:menuItem>
          <h:outputText value="Edit"/>

          <a4j:support event="onclick" action="editChild"
              reRender="dynamicTreePanel, editChildModalPanel"
              oncomplete="Richfaces.showModalPanel('editChildModalPanel')"/>
        </rich:menuItem>
      </rich:contextMenu>
            
      <!-- modal panels -->
      <rich:modalPanel id="addChildModalPanel"
          resizeable="false" height="200" width="330">

        <f:facet name="header">
          <h:outputText value="Add Organisation"/>
        </f:facet>
                
        <h:panelGrid columns="2" rendered="#{addChildModalPanelRendered}"
            columnClasses="verticalAlignTop">
          
          <h:outputText value="Name:"/>
          <h:inputText value="#{addChildOrganisation.name}"/>
                    
          <h:outputLabel value="Description:"/>
          <h:inputTextarea value="#{addChildOrganisation.description}"
              rows="5" cols="30"/>
        </h:panelGrid>
                
        <h:panelGroup style="display:block; text-align:center">
          <a4j:commandButton action="addChildModalPanelConfirm" value="Ok"
              onclick="Richfaces.hideModalPanel('addChildModalPanel')"
              reRender="dynamicTreePanel, addChildModalPanel"
              styleClass="commandButton"/>
    
          <a4j:commandButton action="addChildModalPanelCancel" value="Cancel"
              onclick="Richfaces.hideModalPanel('addChildModalPanel')"
              reRender="dynamicTreePanel, addChildModalPanel"
              styleClass="commandButton"/>
        </h:panelGroup>
      </rich:modalPanel>

      <rich:modalPanel id="editChildModalPanel"
          resizeable="false" height="200" width="330">
        <f:facet name="header">
          <h:outputText value="Edit Organisation"/>
        </f:facet>

        <h:panelGrid columns="2" rendered="#{editChildModalPanelRendered}"
            columnClasses="verticalAlignTop">
          <h:outputText value="Name:"/>
          <h:inputText value="#{editChildOrganisation.name}"/>
                
          <h:outputText value="Description:"/>
          <h:inputTextarea value="#{editChildOrganisation.description}"
              rows="5" cols="30"/>
        </h:panelGrid>

        <h:panelGroup style="display:block; text-align:center">
          <a4j:commandButton action="editChildModalPanelConfirm" value="Ok"
              onclick="Richfaces.hideModalPanel('editChildModalPanel')"
              reRender="dynamicTreePanel, editChildModalPanel"
              styleClass="commandButton"/>

          <a4j:commandButton action="editChildModalPanelCancel" value="Cancel"
              onclick="Richfaces.hideModalPanel('editChildModalPanel')"
              reRender="dynamicTreePanel, editChildModalPanel"
              styleClass="commandButton"
              ajaxSingle="true"/>
        </h:panelGroup>
      </rich:modalPanel>
    </h:form>
  </f:view>
</rich:page>

Listing 6 : dynamicTree.xhtml



As you can see, the context menu’s operate in the same way as before as does the flow transitions and modal dialogs. The main difference here is in the flow transition because rather that create a new child Organisation we are editing an existing one.

It is also worth pointing out the use of the ajaxSingle=”true” attribute on the Cancel commandButton component. The reason this is used is so that the model is not updated during the ajax request.

Adding Support for Removing Child Nodes

Similarly, it is relatively straight forward to add support for removing a child node. To achieve this, we will have to add 2 menu items to the already existing context menus and a transition to the flow step.

What’s Outstanding?

I did notice during this process is that there are some strange happenings that need to be investigated further.

  • When I use a Set as opposed to a List for the child variable in Organisation.java the tree does not bahave as expected.
  • When I try to remove the last child node from the root node the tree does not behave as expected

I realise there is some duplication in the above that should be removed but I do hope you have found this useful and look forward to your comments.


1 Star2 Stars3 Stars4 Stars5 Stars6 Stars7 Stars8 Stars9 Stars10 Stars (1 votes, average: 6.00 out of 10)
Loading ... Loading ...

  • Print
  • Digg
  • del.icio.us
  • DZone
  • Reddit
  • Slashdot
  • StumbleUpon
  • Twitter
  • Facebook
  • Technorati
  • email
  • Google Bookmarks
  • LinkedIn
  • Yahoo! Bookmarks
Advertisement

8 comments

  1. anonym says:

    Looks very interesting. Thanks for the post

  2. Jay says:

    Very clear and solves most of problems. Thanks for the post

  3. RenZo says:

    Interesting post! Congratulations John! But I have a question. Do you know if there is a way to build such Dynamic Tree without using Spring? I am trying to develop the tree using JBossDeveloper Tools using JSF 1.2+RichFaces but I am not achieving success..

    • turnerj says:

      There is of course. Spring webflow is performing two tasks. One is to remember the tree root organisation between requests and the second is to dispatch the action (add, edit, remove) to a particular action handler. Both these things are possible without spring webflow.

      You could try to remove webflow from the article source and then apply this to your own source?

      • RenZo says:

        Hi turnerj! Do you know how can I handle my events without using spring web flow? Like you do in your flow-main.xml? Thanks in advance!

  4. TueDang says:

    Very helpful, thank you

Leave a Reply