Tuesday, October 17, 2006

HowTo work with ADF Faces TreeModel

The ADF tree component (af:tree) uses instances of class TreeModel as model to render its content.

Unfortunately the TreeModel works quick different as common java tree models. Nodes of the tree and aspecialy their children cannot be accessed throw a getter like "getChildren()" which makes navigation and mangement of the tree a bit tricky.

HowTo create a Adf TreeModel

The simples way to create a adf TreeModel instance is to define you own tree node class first:

public class TreeNode
{
  String _text;
  List _children;
 
  public TreeNode(){}
  public TreeNode(String p_text) {_text = p_text};
  public String getText() {return _text;}
  public List getChildren() {return _children;}
}
Afterward you can setup a tree with it and convert it to an adf TreeModel
TreeNode rootNode = new TreeNode();
TreeNode firstLevel = new TreeNode("FirstLevelNode");
rootNode.getChildren.add( firstLevel );
firstLevel.getChildren.add( new TreeNode("SecondLevelNode") );
...
TreeModel _treeModel = new ChildPropertyModel(rootNode.getChildren(), "children");
Note, that the children of the rootNode itself is not displayed but their children. In your Jsp or Xhtml page, this TreeModel instance can be bound to the af:tree component as follows:
<af:tree value="#{myJsfBean.treeModel}" var="node">...</af:tree>

How the ADF TreeModel works

An ADF TreeModel instance always has a "current row" with one can "work" with. The current row can be a any (sub-) node of the tree.

The "current row" can be accessed throw treeModel.getRowKey() which returns an Object (debugging uncovered it to be a list of Object).
The "current row data", which is your TreeNode behind the "currentRow" is accessable as shown in the following line:

TreeNode treeNode = (TreeNode) treeModel.getRowData();
If you want to traverse throw the tree, you have to set the "current row" to a new value and use "enterContainer" and "leaveContainer" to go down or up the tree hierarchy. Let's look at an example on how to find the rowKey for on of you TreeNode's.

Your implementation consists of two methods.
One get's called by the client an manages tree state saving/restoring and uses the second method to do the "real work":

public Object getRowKey(TreeNode p_treeNode)
{
    Object oldRowKey = _treeModel.getRowKey();      // remember old state
    _treeModel.setRowIndex(-1);                     // navigate to "root" node
    Object searchedRowKey = _getRowKey(p_treeNode); // do the job
    _treeModel.setRowKey(oldRowKey);                // restore old state
    return searchedRowKey;
}
The second method iterates over the children of the "current row" by setting the rowIndex with treeModel.setRowIndex(i)in a loop. It compares each "rowData" with p_treeNode and return it on success. If it gets to a row which is not an empty container, it "enters" the container and calls itself recursively.
private Object _getRowKey(ITreeNode p_treeNode)
{
    Object rowKey = null;
    for (int i = 0; i < _treeModel.getRowCount(); i++)
    {
      _treeModel.setRowIndex(i);
      ITreeNode node = (ITreeNode) _treeModel.getRowData();
      if (node.equals(p_menuNode))
      {
        rowKey = _treeModel.getRowKey();
      }
      else
      {
        if (!_treeModel.isContainerEmpty())
        {
        _treeModel.enterContainer();
        rowKey = _getRowKey(p_treeNode);
        _treeModel.exitContainer();
        }
      }
      if (rowKey != null)
        return rowKey;
    }
    return null;
}
The opposite direction, to get your TreeNode for a valid rowKey is simple:
public ITreeNode getTreeNode(Object p_rowKey)
{
  Object oldRowKey = _treeModel.getRowKey();
  _treeModel.setRowKey(p_rowKey);
  ITreeNode treeNode = (ITreeNode) _treeModel.getRowData();
  _treeModel.setRowKey(oldRowKey);
  return treeNode;
}

HowTo set a node expanded

The adf tree component has an attribute "treeState" (an instance of PathSet) which contains a set of "rowKeys". All rowKeys in this pathSet are rendered "expanded".

The best way to set nodes expanded is first to manage the treeState on your own. The PathSet needs a reference to the treeModel so when even you set a new treeModel make sure to update the pathSet of expanded nodes. Here, we do it in the setter:

private PathSet _expandedNodes = new PathSet();
prviate TreeModel _treeModel = null;

protected void setTreeModel(TreeModel model)
{
  _treeModel = model;
  _expandedNodes.clear();
  _expandedNodes.setTreeModel(_treeModel);
}
public PathSet getExpandedNodes() {return _expandedNodes;}
Bind your "expanded" attribut to the tree component, the component will only read the attribut.
<af:tree
  value="#{myJsfBean.treeModel}"
  var="node"
  treeState="#{myJsfBean.expandedNodes}">
  ...
</af:tree>
To set your node expanded, you have to put the rowKey of your node in the set of expanded nodes.
public void setExpanded(ITreeNode p_treeNode, boolean p_expanded)
{
    Object rowKey = getRowKey(p_treeNode);
    if (p_expanded)
    {
      _expandedNodes.getKeySet().add(rowKey);
      List anchestorsOfCurrentNode =
           _treeModel.getAllAncestorContainerRowKeys(rowKey);
      _expandedNodes.getKeySet().addAll(anchestorsOfCurrentNode);
    }
    else
    {
      _expandedNodes.getKeySet().remove(rowKey);
    }
}
Watch out that if the implementation above does not close the complete hierarchy of nodes if one node is set not not expanded.

Put it all Together

Write your self a "TreeModelFacade" which does the dirty jobs and bind your af:tree's only to this facade. Your managed bean can create the tree of TreeNode's and instantiate the TreeModelFacade.

Take this as food for your thoughts:

public class TreeModelFacade
{
  public void setRootTreeNode(TreeNode p_rootNode) {...}
  public TreeModel getTreeModel() {//create ChildPropertyTreeModel form rootNode..}
  public ITreeNode getTreeNode(Object p_rowKey) {...}
  public Object getRowKey(TreeNode p_treeNode) {...}
  public void setExpanded(ITreeNode p_treeNode, boolean p_expanded) {...}
  public void refreshTreeModel() {//after changes in TreeNode tree ...}
  protected void setTreeModel(TreeModel model) {...}
}
References

af:tree (tag documentation)
TreeModel (java doc)
PathSet (java doc)