Jun 6, 2009
Building a contextual recursive tree in Tapestry 5
UPDATE: There have been a few requests for a more complete source code example. The following link is to a demo application of the concepts from this entry, and the source code in this entry has been updated to reflect the content of the demo: Recursive Tapestry Tree Demo Application or fork from GitHub.
We use Tapestry 5 at work. Tapestry is a wonderful, efficient, and powerful framework, but like any piece of technology, there are headaches. The headache we had this past week was how on earth to render a tree of of components where we have to dynamically select the component depending upon the current context within the tree. Here’s the approach we uncovered using three components: a component acting as a container for the tree, another to render individual nodes of the tree (contextually subclassed for different nodes), and a custom delegate component (to turn off parameter caching).
The Container Component
The container component maintains stateful tree information while rendering the tree, and selects the component to render for the current node of the tree.
Here is the container’s template.
<?xml version="1.0" encoding="UTF-8"?> <ul xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <!-- The custom delegate to the component to render based on the context of the current node. --> <t:nodedelegate to="currentNodeRenderer"/> <t:block> <!-- This block contains a set of components to delegate rendering to based on the current node. --> <div t:id="blackNode"> <!-- Delegate to the child component to render. This replaces the component's body. --> <t:nodedelegate to="currentNodeRenderer"/> </div> <div t:id="redNode"> <t:nodeDelegate to="currentNodeRenderer"/> </div> </t:block> </ul>
And the container’s implementation.
public class Tree { @Parameter(required = true) private Node node; @Component(id = "blackNode", parameters = {"node=currentNode", "childPosition=currentChildPosition"}) private RenderBlackNode blackNode; @Component(id = "redNode", parameters = {"node=currentNode", "childPosition=currentChildPosition"}) private RenderRedNode redNode; private Node currentNode; private Map<Node, Integer> childPositions; public Node getCurrentNode() { return currentNode; } public void setCurrentNode(final Node node) { // set the initial child position for the node to 0 if it has not been seen yet. if (!childPositions.containsKey(node)) { childPositions.put(node, 0); } this.currentNode = node; } public int getCurrentChildPosition() { return childPositions.get(this.currentNode); } public void setCurrentChildPosition(final int pos) { this.childPositions.put(currentNode, pos); } /** * Selects the Node component to render based on the currentNode type. * * @return the component to render the current node */ public BaseRenderNode getCurrentNodeRenderer() { BaseRenderNode object = null; if (currentNode instanceof BlackNode) { object = blackNode; } else if (currentNode instanceof RedNode) { object = redNode; } return object; } void setupRender() { childPositions = CollectionFactory.newMap(); setCurrentNode(node); } }
The Custom Delegate
This was the crucial component that unlocked our ability to build the tree. As you can see above, the delegate will be encountered with every node in the tree, and since the type of a child node may change, the delegate needs to be able to change the component that it references. The built-in Delegate component in Tapestry 5 cannot do this, since the “to” parameter is cached. We needed to build a custom Delegate component identical to the built-in one but with the “to” parameter uncached so that the delegate would query the container for what component to render each time the delegate is encountered.
The Node Component (with subclasses)
Each type of node in the tree needs its own component type, but they all extend from an identical base component that is responsible for changing the current node to render as well as updating the index of the next child node to render.
Each subcomponent needs to share this basic structure:
<?xml version="1.0" encoding="UTF-8"?> <div xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <!-- The body is replaced with component(s) for the child nodes. --> <t:body/> </div>
Here’s the base Node component.
public class BaseRenderNode { @Parameter(required = true, cache = false) private Node node; @Parameter private int childPosition; @SetupRender boolean setup() { // prevents rending with the node parameter is null. return node != null; } @BeforeRenderBody boolean beforeChild() { // the node has children, render the body to render a child final boolean render = node.getChildren().size() > 0; if (render) { // sets the container's currentNode to the node's child at the given index. node = node.getChildren().get(childPosition); } return render; } @AfterRenderBody boolean afterChild() { // increment the child position, afterRender on the child will have the container's currentNode set back to the node before the body was rendered. childPosition = childPosition + 1; // return true on last child index, finishing the iteration over the children, otherwise re-render the body (to render the next child) return node.getChildren().size() <= childPosition; } @AfterRender void after() { // set the currentNode to the parent after render (pop the stack) if (!node.isRoot()) node = node.getParent(); } }
Conclusion
We have seen how a tree-like structure can be rendered through Tapestry 5. Tapestry’s ability to dynamically render static templates made this possible, but we needed to turn off a caching optimization in the Delegate component in order to increase the dynamism.
Hi,
I’ve followed your guide but I don’t understand these following things
+ element = element.getChildren().get(childPosition);
what is the element?
+ How to remove caching? Is NodeDelegate like this
public class NodeDelegate {
@Parameter(required = true,cache=false)
private Object to;
Object beginRender() {
return to;
}
}
+ Can you expose your source code? I’d appreciate your help
“element in “element = element.getChildren().get(childPosition)” was a mistake. That should be a reference to the “node” parameter of the component. Originally, I was using “element”, but thought “node” was a clearer term for the post and missed that change.
And yeah, that’s exactly what the NodeDelegate looks like.
I’m using “Node” terminology for both the data object and the component, hopefully that’s not confusing.
Thank for your quick reply,
I still get headache to display tree correctly)
Now i can display simple tree recursively.
Simple tree is the following form
Root
Node1
Node 1.1
Node 1.2
Node2
For advance tree
Root
Node1
Node 1.1
Node 1.2
Node2
Node 2.1 ( can’t display)
Because I’m using only one index variable to loop. I find it hard to do recursively ( myself).
But in summary, Your solution works great. Thank for your tutorial.
(
If you could just post all your source code, it will be much more helpful.
Could you please help posting the complete source code…
A complete source code example has been added.