Tuesday, June 27, 2006

MyFaces Tree2 - Creating a lazy loading tree

Note: this code is hosted at http://sourceforge.net/projects/jsf-comp under the AjaxAnywhere download. As a result, this blog post's content may be out of date compared to that source.

Although the MyFaces Tree2 WIKI discusses lazy loading children tree nodes, it does not cover the use case of if you don't know if there are children or not. Recently, I had a use case where I was loading children using a WebService that would return me contents of a node per-call. Obviously for performance reasons, I wanted to keep the number of calls to a minumum.

My approach:
Use AJAX via AjaxAnywhere and server-side toggling with Tree2 to lazy load the tree nodes. When asked for the children of a node that isn't loaded, I would load the nodes then and there.

My use case was wrapped around a content management solution, so it was file/directory based nodes.

What was needed:
  • Custom tree node for "directory nodes"
  • Custom tree nodes for "file nodes" that could be selected
Note: this blog will not discuss AJAX and enabling the tree in server side mode, that would be another discussion entirely

Step 1: create a "lazy loading" tree node that we can extend:

public class BaseTreeNode implements TreeNode { private BaseTreeNode parent; private String identifier; private String name; private String type; protected List<BaseTreeNode> children; protected TreeModel model; public BaseTreeNode() {} public BaseTreeNode(TreeModel model, BaseTreeNode parent, String type, String identifier, String name, boolean leaf) { this.model = model; this.type = type; this.parent = parent; this.identifier = identifier; this.name = name; if (leaf) children = Collections.emptyList(); } /** * @return Returns the parent. */ public BaseTreeNode getParent() { return this.parent; } /** * @see org.apache.myfaces.custom.tree2.TreeNode#isLeaf() */ public boolean isLeaf() { return getChildCount() == 0; } /** * @see org.apache.myfaces.custom.tree2.TreeNode#setLeaf(boolean) */ public void setLeaf(boolean leaf) {} /** * @see org.apache.myfaces.custom.tree2.TreeNode#getChildren() */ public List<BaseTreeNode> getChildren() { if (children == null) children = loadChildren(); return children; } /** * @see org.apache.myfaces.custom.tree2.TreeNode#getType() */ public String getType() { return type; } /** * @see org.apache.myfaces.custom.tree2.TreeNode#setType(java.lang.String) */ public void setType(String type) { this.type = type; } /** * @see org.apache.myfaces.custom.tree2.TreeNode#getDescription() */ public String getDescription() { return name; } /** * @see org.apache.myfaces.custom.tree2.TreeNode#setDescription(java.lang.String) */ public void setDescription(String description) {} /** * @see org.apache.myfaces.custom.tree2.TreeNode#setIdentifier(java.lang.String) */ public void setIdentifier(String identifier) {} /** * @see org.apache.myfaces.custom.tree2.TreeNode#getIdentifier() */ public String getIdentifier() { return identifier; } public int getIndex() { return (parent == null) ? 0 : parent.getChildren().indexOf(this); } /** * @return Returns the name. */ public String getName() { return this.name; } public String getFullPath() { StringBuilder sb = new StringBuilder(name); for (BaseTreeNode node = getParent(); node != null; node = node.getParent()) sb.insert(0, '/').insert(0, node.getName()); return sb.toString(); } public String getNodePath() { StringBuilder sb = new StringBuilder(Integer.toString(getIndex())); for (BaseTreeNode node = getParent(); node != null; node = node.getParent()) sb.insert(0, TreeModel.SEPARATOR).insert(0, node.getIndex()); return sb.toString(); } public boolean isExpanded() { if (model == null) return true; return model.getTreeState() .isNodeExpanded(getNodePath()); } /** * @see org.apache.myfaces.custom.tree2.TreeNode#getChildCount() */ public int getChildCount() { if (children == null && !isExpanded()) return 1; if (children == null) children = loadChildren(); if (children == null) return 0; return children.size(); } /** * @see java.lang.Object#toString() */ @Override public String toString() { return name; } protected List<BaseTreeNode> loadChildren() { return null; } }

This base class takes most of the tedious work away from having to create the lazy loading functionality. In the constructor, a "lazy" parameter is provided for nodes that the user knows will not have children (like a file node in this example). This node also includes code for working with the Tree2 default TreeModel and TreeState by implementing a "getNodePath" function that works with node indexes instead of just node IDs.

Step 2: create the directory node (inner class):

public class FolderNode extends BaseTreeNode { FolderNode(TreeModel model, BaseTreeNode parent, String name) { super(model, parent, "folder", name, name, false); } @Override protected List<BaseTreeNode> loadChildren() { return <Your business code class here>.loadChildren(this); } }

The implementation of the folder is simple. I have not shown my code, but I have this node in my code as an inner class that I have this "loadChildren" method that calls my Web service. In that method, I simply create new nodes an add them to the parent node.

The key here is how the tree2 works and how the renderer works (this is source code specific, so if Tree2 was changed drastically enough, this may break). The tree2 renderer looks only at the getChildCount method, not the getChildren method when rendering. Meaning that if getChildCount should return 0, getChildren will never be called unless the node is expanded. In my base node, you will see that I am returning 1 child as the child count if the children have not been loaded and the node is not expanded. This will cause tree2 to render a plus sign icon next to my node (indicating that there are children).

If the node is expanded, and the children are not loaded, my base node then calls the load children method. This will force the accurate count of nodes to be returned for this node. If I were to only put my loading code in the getChildren, the tree would not render correctly, as getChildCount is always called first in the renderer.

What ends up being the result is that when the plus sign is clicked, the node is exanded in the tree model's tree state. Then the tree is rendered (having the getChildCount method called). I load the children, and return the correct count to the renderer which then proceeds in calling getChildren and building the HTML.

This will work if there are actually no children (the plus sign simply dissappears), or if there are many children (not the "1" that was originally reported as the number of children).

FYI, here is the "file" node code:

public class FileNode extends BaseTreeNode { FileNode(TreeModel model, BaseTreeNode parent, String name) { super(model, parent, "file", name, name, true); } }
The XHTML is as follows:
<aa:zoneJSF id="treeZone"> <my:ajaxTree value="#{contentMgmtBean.treeModel}" ajaxZone="treeZone" var="_node" clientSideToggle="false" varNodeToggler="t" showRootNode="false"> <f:facet name="folder"> <t:panelGroup> <t:graphicImage styleClass="treeNodeIcon" url="#{_node.expanded ? '/images/contentMgmt/openfolder.gif' : '/images/contentMgmt/closedfolder.gif'}" /> <t:outputText styleClass="folderNodeText" value="#{_node.name}" /> </t:panelGroup> </f:facet> <f:facet name="file"> <my:ajaxCommandLink ajaxZone="treeZone,myResultZone" actionListener="#{contentMgmtBean.nodeSelected}" styleClass="fileNodeLink#{ contentMgmtBean.currentFileName eq _node.name ? ' selectedNode' : ''}"> <t:updateActionListener property="#{contentMgmtBean.currentFileName}" value="#{_node.name}" /> <t:graphicImage styleClass="treeNodeIcon" url="/images/contentMgmt/file.gif" /> <t:outputText styleClass="fileNodeText" value="#{_node.name}" /> </ost:ajaxCommandLink> </f:facet> </my:ajaxTree> </aa:zoneJSF>

Example of using the node as an inner class:

public class BeanClass { private TreeModel treeModel; public List<BaseTreeNode> loadChildren(FolderNode parent) { List<BaseTreeNode> children = new ArrayList(); List<String> folders = ; // load from web service for (String folder : folders) children.add(new FolderNode(treeModel, parent, folder)); List<String> files = ; // load from web service for (String file : files) children.add(new FileNode(treeModel, parent, file)); return children; } public class FolderNode extends BaseTreeNode { ... protected List<BaseTreeNode> loadChildren() { return loadChildren(this); } } }

Update This code has been moved to a jsf-comp component. Go to http://sf.net/projects/jsf-comp to download. An example war file is included in the release

© Copyright 2006 - Andrew Robinson.
Please feel free to use in your applications under the LGPL license (http://www.gnu.org/licenses/lgpl.html).


dong-soo said...

I tried to use tree2 in server side toggle mode but it didn't expand when I click node.
is there any extra configuration for tree2?. I just followed tree2 example in zip file.
I really appreciate it if you can help me.

Andrew said...

You can get the code for this article with a working demo (war file) at jsf-comp.sourceforge.net and download the "AjaxAnywhere" release [1]. For other tree2 questions, ask the author or post a question on the myfaces mailing list.

[1] http://sourceforge.net/project/showfiles.php?group_id=137466&package_id=197375

GUDIVADA said...

I have to diplay dynamic data from database in hierarchial format using JSF.can u help me

Andrew said...

There is a full working WAR file in the jsf-comp project in the "all" download. You can use that to understand how to setup the pages and the tree. You can then just plug-in the database code to load the nodes.
If you need some more help, I recommend posting to the MyFaces user's mailing list
(I will answer questions there as well)

GUDIVADA said...

can u give me some examples on displaying hierarchial chart

Robert said...

Hello Andrew Robinson,

I'm trying to make your component, named jsfCompAA, work with Tomahawk 1.1.5 and sun RI.

Tomahawk 1.1.5 doesn't need the myfaces core libraries.

I replaced
tomahawk 1.1.3 by tomahawk 1.1.5 and removed the files myfaces-api-1.1.4-SNAPSHOT.jar and myfaces-impl-1.1.4-SNAPSHOT.jar. I added Sun's jsf-api.jar and jsf-impl.jar.

I keep getting a error in the browser if I expand the tree:
at org.apache.myfaces.shared_tomahawk.rederkit.html.HtmlRendererUtils.isAllowedCdataSection(HtmlRendererUtils.java:1119)

What is the function of the tomahawk.taglib.xml file?
If I remove this file the component still works

Should it be pretty easy to update tomahawk 1.1.3 to tomahawk 1.1.5?

Thank you in advance.


Andrew said...

I don't think that I have checked this code since myfaces 1.1.1. It should work fine on 1.1.4 or 1.1.5 (I would avoid the snapshots). The tomahawk.taglib.xml is for facelets support. Without it, the component will not be created. As for the Tomahawk error, I would post on the myfaces users list as it is a myfaces error (I am also very active on that list and can help you there)

Robert said...

Thanks for your quick reply.
In a certain way I have made the component working standalone.

Now I'm trying to use this component in a sun RI JSF project.

Is there a way to get rid of the xhtml extension and use the component directly in a jsf file?

And why did you use facelets support? Is there a (better)alternative for the facelets?


Andrew said...

Facelets uses XML files (I use an xhtml extension) as opposed to JSP. I don't like JSP at all and choose to not use it. Instead I use facelets. So if you want to use JSP instead of facelets, you have to create a component tag for the component and a tld file as well. Just follow JSF component building guides on how to do it.

Robert said...

Hello Andrew,

This is my last questions about this component.

I added a tld and component tag to run the component from a jsp page.

I can see the tree but can't open the nodes. If i try to open the node it give javascript error:
document.forms.exampleForm['lazyAjaxTree:org.apache.myfaces.tree.NAV_COMMAND'] has no properties

If I compare the html code in the browser generated by facelets to the code generated by jsp I noticed
there's some generated javascript and html missing in the jsp variant.

Missing code
A couple of hidden inputs and javascript:

function clear_exampleForm() {
var f = document.forms['exampleForm'];

I searched the internet nut didn't found a solution.
Do you have any ideas?


Andrew said...

That is a question for the MyFaces user list. If it still doesn't work after this comment, please ask the questions on that list.
It looks like you are missing the javax.faces.component.UIInput.CONVERSION_detail = "{0}": Conversion error occurred. extension filter being setup correctly in your web.xml for the JSP version. I am not 100% sure, you may get better feedback from other MyFaces Developers on the list though.

Jim said...

Hi Andrew,

The 2nd example from myface's wiki for lazy loading doesn't work too well for me. The root node already has a list of folder nodes from its own getChildren(); however, when I click + to expand the root node, tree2 actually calls getChildren() of each of the folder nodes and jump one level ahead. It results in dozens of expensive web service calls to get the children of each node.

By looking your implementation, I couldn't tell if it has solved this look-one-level-ahead problem. The war seems to be loading nodes very quickly, but then again, it was only adding dummy nodes. Also, my project will be using tree2 in a portal, and am not sure if ajaxAnywhere would introduce incompatibilities? And finally, is the LGPL compatible with Apache-style license which my project is under?

I look forward to any comments/suggestions/advice. Thanks.

-- Jim

Andrew said...

The best thing to do is have a look at my code on the jsf-comp web site:


I stopped using Tree2 a long time ago, and now use Trinidad instead which has better framework features.

As for portals, I have no idea if AjaxAnywhere is compatible. I know that Trinidad is fairly portal compatible and there is an active developer that is a portal expert on that team.

As for LGPL vs Apache2, there are some conflicts. You cannot release an Apache2 product with an LGPL dependency to my knowledge.

Best thing is to ask around on the MyFaces mailing list