[Image] EchoPoint
Helping you build truly dynamic and stateful web applications!
nothing

Using the EchoPoint Tree Component

By Brad Baker
Wednesday, July 09, 2003

The design kudos for the Tree component must go to Sun and the Swing design team.  The EchoPoint Tree component is a HTML rendered implementation using their API design.

Introduction

The Echo/Echo Point mailing lists have generated a flurry of support emails regarding the echopoint.Tree component.  It seems everyone wants to use this component in their applications.

This is with good reason.  A hierarchical view of data makes a lot of sense in many applications.  The Tree component provides just that, a hierarchial Tree of text and/or other components.

Tree in action
[Image]

The EchoPoint Tree component is based heavily on the standard javax.swing.JTree component.  The design of the Swing JTree component was deliberately emulated so that anyone who was familair with the Swing JTree could use the EchoPoint Tree.  This also help ensure a best pratice design was used for the EchoPoint Tree component.

The classes you are interested in are echopoint.Tree and all of the classes in echopoint.tree package.  Have a quick squizz at these to see what is available.

http://echopoint.sourceforge.net/javadoc/docs/echopoint/Tree.html

http://echopoint.sourceforge.net/javadoc/docs/echopoint/tree/package-summary.html

 

 

Getting up to Speed

Now to get up to speed quickly on Tree controls, read the following Swing Tutorial documentation.  It was written by professional "trainers" and most of what they say applies to the EchoPoint Tree. 

http://java.sun.com/docs/books/tutorial/uiswing/components/tree.html

Now that you have read this Swing Tutorial article (you did read it didn't you?  I am going to reference it, so you may want to go back and read it!) I will explain what parts of it apply to the EchoPoint Tree and which parts don't.

In the section "Creating a Tree", it talks about DefaultMutableNode.  This is the class that you will use most often to create a hierarchical set of nodes.  This class implements MutableTreeNode, which itself extends TreeNode.  The end result of this is that you can chain together a series of TreeNodes, with one at the "root" to form you tree.

The other interface to be aware of it TreeModel.  The Tree component requires a TreeModel in order to provide the nodes within it and their hierarchical relationship.  The class DefaultTreeModel implements TreeModel and allows you to add MutableTreeNode objects to it.  Again this is the class you will most likely use to build the model for the Tree component.

DefaultMutableNode also allows you to set a "user object" into the node.  This is extremely useful because it allows you to build a hierarchy of nodes with references to objects that in themselves may not be hierarchical.  The getUserObject() and setUserObject() methods are used for this purpose.

It is possible to have a TreeModel that is not based on a series of interlinking TreeNodes.  In this case you must develop your own TreeModel and have a series of objects that form a hierarchy.  Your custom tree model will be responsible for return the child nodes etc...

Tree Rendering

Once you have your TreeModel and TreeNodes in place, the Tree component will be in a position to render them into an expanding list of nodes.  It does this by using an object that implements TreeCellRenderer.  In may cases you can use the DefaultTreeCellRenderer that is provided with EchoPoint.

The TreeCellRenderer is asked by the Tree component to return a "representation" of each node in the TreeModel, when they are needed.  This means whenever a tree node is shown or is expanded or collapsed, the TreeCellRenderer may be called.  The interface looks like :

public interface TreeCellRenderer {

   public String getIdString(Tree tree, Object value);

   public
Component getTreeCellRendererComponent(Tree tree, Object value,boolean selected, boolean expanded,boolean leaf);

   public Label getTreeCellRendererText(Tree tree, Object value,boolean selected, boolean expanded,boolean leaf);
}

The contract that this interfaces has with the Tree requires some explaining.  First off every node requires an unique identifier.  This is to so the Tree component can tell which node has been clicked on etc..  Then once the identity of a node has been established, then the Tree will make one of two possible calls.

First it will call the getTreeCellRendererComponent() method with a given node.  The renderer is expected to provide a unique component that represents this tree node or null if its not using this technique.  Providing a unique component incurs some memory performance because a new Component object must be created for each tree node.  However this also offers the greatest flexibility when displaying a Tree.

If the getTreeCellRendererComponent() method returns null, then the getTreeCellRendererText() method will be called.  This is expected to return a Label component that represents the text and icon of a tree cell.  The Label returned does NOT have to be unique because the Tree component only uses the public properties of the Label to render a textual and iconic representation of the tree cell.  Specifically the background color, foreground color, font, text and icon properties are used. 

This allows the same Label object to be returned for each tree cell, with different property values.  This save a considerable amount of memory (since only one component is created) and can be used for most Tree implementations because tree cells usually consist of an icon followed by text.

The DefaultTreeCellRenderer class does exactly this.  It is in fact derived from Label, and returns itself in the getTreeCellRendererText() method. 

It also has a nice feature where if the user object contained within the tree node is in fact a Component, it returns that component during the getTreeCellRendererComponent() method call.  If its not a Component, then it returns a toString() representation of the user object in the getTreeCellRendererText() method call.  This provides what you would expect in most cases.

Customizing Tree Rendering

To customize tree rendering you need to customize the TreeCellRenderer in use.  The easiest way to do this is to derived your own class from DefaultTreeCellRenderer and set it into the Tree component.  Inside you derived class call the default processing and then customize the returned component as you see fit.  For example imagine a Tree where you want all text that starts with EchoPoint to be bolded.  You might do this :

public class MyTreeCellRenderer extends DefaultTreeCellRenderer {

   Font boldFont =
new Font(Font.VERDANA,Font.BOLD,10);

   public Label getTreeCellRendererText(Tree tree, Object node, boolean sel, boolean expanded, boolean leaf) {
  
         Label l =
super.getTreeCellRendererText(tree, node, sel, expanded, leaf);
         if (l.getText().startsWith("EchoPoint")) {
            l.setFont(boldFont);
         }
         return l;
  }
}

If all you want to customize is the appearance of the cell text or icon, the overriding the getTreeCellRendererText() method is for you.  The Tree rendering code takes the Label returned and uses it as a template or stamp for producing the Tree cell.  It uses the properties of the Label to discover cell attributes such as font, foreground and background color. 

The Tree rendering code does not add the returned Label to the component hierarchy and hence you can return the same Label object during the many calls to the TreeCellRenderer. 

Tree Cell Icons

When the Tree rendering code needs to drawn a line between cells then it uses the TreeIcons interface object of the Tree to provide the right images.  Also when the DefaultTreeCellRenderer needs to provide an icon for an expanded tree cell, it uses the TreeIcons interface.

The Tree is created with a DefaultTreeIcons object which, as you might guess, implements TreeIcons.  This contains images for expanded and collspased nodes as well as the lines needed to connect this cells.

Advanced Tree Rendering

.If you want, however, to provide a special Component for each tree cell, then you have two options. 

Firstly if you use the DefaultMutableTreeNode and set the user object to be a Component, then the DefaultTreeCellRenderer class will return this component during rendering.

Otherwise you need to provide your own TreeCellRenderer that returns components during the getTreeCellRendererComponent() method.   Simplistically you might do this :

public class MyTreeCellRenderer extends DefaultTreeCellRenderer {
   public Label getTreeCellRendererComponent(Tree tree, Object node, boolean sel, boolean expanded, boolean leaf) {
         return new Label(node.toString());
   }
}

There are problems with the above approach.  First the node might be null, so this above code could throw  a NullPointerException.  But hey thats just a coding oversight.

More importantly the Tree rendering code will call the TreeCellRenderer each time the state of a tree cell changes.  A tree cell's state may change in response to it being uncovered, expanded, collapsed or selected.  Therefore the above code will return a new Label each user interaction.  This might not be very efficient and will involve many components coming in and out of existence.

The Tree component attempt to minimize the number of times it calls the TreeCellRenderer. It does not call the renderer during every server interaction, rather it is called only when the underlying TreeModel changes or the TreeSelectionModel changes or a tree node is expanded, collapsed.  However in order to be flexible, the Tree must call the TreeCellRenderer many times within the life time of a Tree.

You must be aware of this when you implement your custom TreeCellRenderer.  If your representation of a tree cell does not change when its selected, expanded or collapsed, then you may want to implement some sort of cache that maps tree nodes to previously returned components. 

Maybe something like:

public class MyTreeCellRenderer extends DefaultTreeCellRenderer {

   Map componentMap = new WeakHashMap();

    public Component getTreeCellRendererComponent(Tree tree, Object node, boolean selected, boolean expanded, boolean leaf) {
          Label l = (Label) componentMap.get(node);
          if (l == null) {
                l = new Label(node.toString());
                componentMap.put(node,l);
          }
          return l;
   }
}

Listening For Selection Events

There are two main ways to listen for events when using the Tree component. 

The first is to create your own TreeSelectionListener that is informed every time  a new tree cell selection happens.  The TreeSelectionListeners attached to the tree are always informed of tree cell selections.

This is explained in more detail in the Swing tutorial under Responding to Node Selection.

The other way is to use objects that implement TreeNode inside tree.  EchoPoint has added a getActionCommand method to the TreeNode interface (the standard Swing interface does not have this method) and this can be used to raise ActionEvents when a Tree node is clicked on.  These action commands naturally form a hierarchy such that if a node does not have an action command associated with it, then the parent node is searched and so on.  The action command on lowest level node in the tree is used to raise a new ActionEvent.

Any ActionListeners attached to the Tree component will be notified.

Tree Cell Selection And Custom TreeCellRenderers

There is an issue with Tree cell selection and providing your own custom TreeCellRenderer implementation.  If you return a Component via the TreeCellRenderer.getTreeCellRendererComponent() method call, then the Tree cell selection can no longer work.

The Tree rendering code has no knowledge of what constitutes a "selection" on the cell Component.

Imagine you have returned a DatePicker component for a tree cell.  What does it mean to "select" this component?  On what part do you need to click to generate a selection?  Shouldn't clicks go through to the DatePicker so it can perform its function of allowing the user to select a date?

So for this reason, if the TreeCellRenderer.getTreeCellRendererComponent() method call returns a non null Component, then Tree selection is no longer applicable.  Expanding and collapsing nodes still works but Tree selection does not.

Also another issue to consider when developing your own custom TreeCellRenderer is the lifecycle of the component you return in the getTreeCellRendererComponent() method. 

The Tree code will try to minimize the number of times the TreeCellRenderer is called, but be aware that it will be called whenever a cell is expanded or collapsed.  So you must not assume that a Cmoponent for a tree cell will only be asked for once.

Server Side vs Client Side Trees

The Tree component can be operated in two modes.  In Server Side mode allows for the full functionality of the Tree component.  All expansion and selection events can be captured in the server side code.  Nodes can be provided on an as needed basis.

In Client Side mode, the Tree builds itself completely and does not interact with the server when the nodes are expanded or collapsed.  All the Tree nodes are renderer regardless of whether they are visible at render time.  This provides a fast acting Tree experience to the end user.  This is especially useful if you Tree node depth is quite low.  A client side tree can be used to provide a hierarchical menu to users.

There are some restrictions on client side Trees however.  It does not raise and expansion events and it cannot change the icon of a node truly dynamically based on expansion or not.  What is does is cache both the expanded and collapsed icons at render time.

However if your TreeModel has many levels and many nodes in it, then a client side Tree may not be what you want.

Just-In-Time Tree Nodes

If your tree model is very deep or possibly has infinite nodes, then you will not be able to use a client side Tree.  The reason for this is that the client side Tree requires that all nodes be available and known at render time. 

For example, imagine you have node hierarchy where object A points to B which points to C which in turn points back to A.  You could represent this via a Tree by making B a child of A and then C a child of B and then recursively by making A a child of C and so on.  Each time you expand a child tree cell, you are extending the number of nodes ad infinitum.

You can however provide tree nodes on an as needed basis and remove them when they are no longer needed.  You can do this via the TreeWillExpandListener interface or via the TreeModel interface. 

The TreeWillExpandListener is called when a tree is about to expand or collapse.  You can throw a ExpandVetoException to prevent it or you can add child nodes or remove child nodes inside your handler as appropriate.  The TreeExpansionListener interface will be notified after the Tree has expanded or collapsed.

You can find out more about this in the Swing Tutorial article called How to Write a Tree-Will-Expand Listener.

The other dynamically add child nodes is to have a custom TreeModel implemented that provides child nodes as needed.  Since the TreeModel can return any objects, not just TreeNodes, the objects do not have to maintain forward and backward points etc.  This technique however requires the most coding.

[Image]

If you look at the EchoPoint Viewer demo application show here, it use a TreeWillExpandListener to add nodes and remove nodes as applicable.  This application shows the components inside an Echo application, itself.  Since all components point back to the parent EchoInstance, and the EchoInstance points to the components within itself, the nodes in the Tree theoretically can go on forever.  Therefore the application dynamically builds the TreeModel as needed and destroys child nodes when a node is collapsed.

Notice in the picture that nodes in the Tree are recursive.  The Panel has a pointer to the EchoInstance which contains Windows, which have points to the EchoInstance and so on.

Adding and deleting nodes on demand allows the user to drill down without expanding using excessive memory as the nodes are added as required.

 

 

 

 

 

 

Swing JTree Features That Do Not Apply

Some of the features of the Swing JTree do not apply to the EchoPoint Tree.  These Swing JTree features are discussed in the Swing Tutorial but they are not available in the EchoPoint Tree.  Specifically the features not avilable are :

  • Customising the Look of the Tree via properties

The Tutorial talks about customising the Look of the Tree by setting client properties. 

tree.putClientProperty("JTree.lineStyle", "Horizontal");
tree.putClientProperty("JTree.lineStyle", "Angled");
This is not how it is done with the EchoPoint Tree.  Instead you should use the TreeIcons interface to provide the "icons" that make up the Tree cell lines and expansion icons.  A DefaultTreeIcons class is provided that hold the default Tree icons.  You can use this as a starting point.
  • Having Editable Cell Text

The Swing tutorial talks about allowing cell text to be edited by call the tree.setEditable() method.  This is not available in the EchoPoint tree. 

If you want cell text to be editable, then one way might be to return a TextField for each cell via the TreeCellRenderer.getTreeCellRendererComponent() method.  However by default tree cell text is not editable. 


Home

SourceForge Logo

The EchoPoint project is kindly hosted by SourceForge