Master/Detail using af:tree for master and af:form for detail

A question on OTN JDeveloper and ADF forum about a sample for a master/detail navigation using a tree as master and a form as detail cought my attention. I quickly setup a sample I like to share in this post.

Frank Nimphius wrote an article How-to select multiple parent table rows and synchronize a detail table with the combined resul about this which is quite complex as it not only shows how the synchronization is done but adds CRUD operation to the tree too.
My sample is for beginners and only shows how to do the synchronization of the tree navigation and the detail from. If you need more functionality please refer to Frank’s article.

Problem description
At first the use case sounds easy. However, using a tree has it’s pitfalls. The tree navigation doesn’t synchronize the child node iterators with the node selection. This means that when we select a child node that the current row of the child iterator is not set to the selected row. This is shown in the gallery below:


As we see, in the second picture the form show the wrong data for the selected node. The form shows the first child row of the selected parent node.

Page layout
the sample uses a panel splitter which holds the af:tree on the left and the detail af:form on the right panel. This is shown in the gallery below:


The af:form has a partialTrigger set which listens to updates of the af:tree. The af:tree was build by dragging the Departments from the data control onto the left panel of the splitter. This created the markup
Default selectionListener of af:tree

Default selectionListener of af:tree


with the default selectionListener marked in blue. This is where we need to make the change.

Solution
To make the use case work, we have to use a custom selectionListener for the af:tree. The gallery below shows how a request scoped bean and the selectionListener is created:


The last image shows that the bean is automatically registered in the task flow (in adfc-config.xml in this case).
Now that we have created the selectionListener method in the bean, we can code it like

    /**
     * Custom managed bean method that takes a SelectEvent input argument
     * to generically set the current row corresponding to the selected row
     * in the tree.
     *
     * This method is not generic as it uses the normal binding.iterator.model.makecurrent the ui component uses.
     * The child iterator must be known too to select the child not in the chile view.
     *
     * @param selectionEvent object passed in by ADF Faces when configuring
     * this method to become the selection listener
     */
    public void onTreeNodeSelection(SelectionEvent selectionEvent) {
        //original selection listener set by ADF
        //#{bindings.Departments.treeModel.makeCurrent}
        String adfSelectionListener = "#{bindings.Departments.treeModel.makeCurrent}";
        //make sure the default selection listener functionality is
        //preserved. you don't need to do this for multi select trees
        //as the ADF binding only supports single current row selection

        /* START PRESERVER DEFAULT ADF SELECT BEHAVIOR */
        FacesContext fctx = FacesContext.getCurrentInstance();
        Application application = fctx.getApplication();
        ELContext elCtx = fctx.getELContext();
        ExpressionFactory exprFactory = application.getExpressionFactory();

        MethodExpression me = null;
        me = exprFactory.createMethodExpression(elCtx, adfSelectionListener, Object.class, new Class[] { SelectionEvent.class });
        me.invoke(elCtx, new Object[] { selectionEvent });

        /* END PRESERVER DEFAULT ADF SELECT BEHAVIOR */
        RichTree tree = (RichTree)selectionEvent.getSource();
        TreeModel model = (TreeModel)tree.getValue();

        //get selected nodes
        RowKeySet rowKeySet = selectionEvent.getAddedSet();
        Iterator<Object> rksIterator = (Iterator<Object>)rowKeySet.iterator();
        //for single select configurations,this only is called once
        while (rksIterator.hasNext()) {
            List<Object> key = (List<Object>)rksIterator.next();
            JUCtrlHierBinding treeBinding = null;
            CollectionModel collectionModel = (CollectionModel)tree.getValue();
            treeBinding = (JUCtrlHierBinding)collectionModel.getWrappedData();
            JUCtrlHierNodeBinding nodeBinding = null;
            nodeBinding = treeBinding.findNodeByKeyPath(key);
            Row rw = nodeBinding.getRow();
            //print first row attribute. Note that in a tree you have to
            //determine the node type if you want to select node attributes
            //by name and not index
            String rowType = rw.getStructureDef().getDefName();

            // check the node type as we don'T have to do anything is the parent node is selected
            if (rowType.equalsIgnoreCase("DepartmentsView")) {
                logger.info("This row is a department: " + rw.getAttribute("DepartmentId"));
            } else if (rowType.equalsIgnoreCase("EmployeesView")) {
                // for the child node we search the row which was selected in the tree
                logger.info("This row is an employee: " + rw.getAttribute("EmployeeId"));
                Object attribute = rw.getAttribute("EmployeeId");
                // make the selected row the current row in the child iterator
                DCBindingContainer dcBindings = (DCBindingContainer)BindingContext.getCurrent().getCurrentBindingsEntry();
                DCIteratorBinding iterBind = (DCIteratorBinding)dcBindings.get("EmployeesOfDepartmentsIterator");
                iterBind.setCurrentRowWithKeyValue(attribute.toString());
            } else {
                // tif you end here your tree has more then two node types
                logger.info("Huh????");
            }
            // ... do more useful stuff here
        }
    }

Line 15 holds the original selectionListener expression from the af:tree. This we need to select the master node and row.
Lines 16-30 preserve the original function by executing the expression.
lines 31-38 check if there are selected nodes in the tree. Here we have to remember that a selected node in a tree isn’t just a rowKey the selectionListener always returns a set of nodes. If the tree is set to single selection the set contains exactly one node.
lines 39-49 get the data model of the tree and getting the node data (or row) and it’s type. This type we use to distinguish between master and detail selection.
Lines 51-65 do exactly this. In case of a master node (Departments) there is nothing to do as the default iterator does all the work. In case of a detail node (Employees) we have to do the magic.
Lines 54-61 Here we get the PK of the child row from the node and set the current row in the child iterator to this row (line 61).

That’s it. when we now run the application we see that the detail form synchronizes with the selected node in the tree.

The sample needs the HR DB schema. You can download the code for the sample, which was build using JDeveloper 11.1.1.9.0, from GitHub. Please note that if you run the sample in your environment, that you have to change the DB connection to the HR DB schema according to your environment.