Richard Bolkey – Blog

Icon

Thoughts of a plain old java developer.

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

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.

Category: Programming

Tagged: ,

6 Responses

  1. Van says:

    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

    • rbolkey says:

      “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.

  2. Van says:

    Thank for your quick reply,
    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.
    ( :) I still get headache to display tree correctly)

  3. Simon says:

    If you could just post all your source code, it will be much more helpful.

  4. Kumar says:

    Could you please help posting the complete source code…

  5. rbolkey says:

    A complete source code example has been added.

Leave a Reply