Fixing the ADF Faces Tree Model Binding – Leaf Nodes without Folder Icon

7

Some of my past writings on this blog were on ADF Faces and more specifically on the ADF Faces Tree component. When bound to an ADF Data Control, the Tree component has a very annoying habit: every node is shown at all times as having details. Even leaf nodes with no detail nodes whatsoever are displayed as if they were container nodes; this is even the case when the node has been expanded and clearly demonstrates an overwhelming lack of children. You can see an example of this behavior here – for an HRM tree with DEPT and EMP based nodes:

 

 

In this article, I will describe a solution to this problem, created with help from Steve Muench and in collaboration with Rob Brands. It turns out to be an exercise in the ADF Model framework and is not suitable for the faint of heart. See for more background on the problem and the road to a solution my previous article Building an ADF Faces Tree Component with mixed (DEPT and EMP) nodes in 5 minutes using JHeadstart 10.1.3.

....
Let’s first create a simple application with a tree component, just like the one shown above:

Creating a Data Driven Web Application with ADF Tree Component 

In rapid succession the steps to get to the point where we have a ADF Tree component in our Web Application, based on database tables, with that annoying every-node-is-a-container behavior:

1. Start JDeveloper 10.1.3

2. Create New Application, new Project with Web Technology Template (ADF BC and JSF); a Model and a ViewController project are created

3. In the Model Project:

 

  • Create new Business Components from Tables: create or select a connection to the SCOTT schema, select tables EMP and DEPT, create default EntityObjects and ViewObjects, close the wizard
  • Edit the Emp and Dept EntityObjects: set Empno respectively Deptno to be the Primary Key attribute and uncheck the Primary Checkbox for the RowId attribute in both EntityObjects
  • Edit the EmpView and DeptView ViewObjects and remove the RowId attribute from both
  • Create ViewLinks from Dept to Emp (based on DeptView.Deptno = EmpView.Deptno) and from Emp to Emp (based on Emp.Empno = Emp.Mgr)
  • Edit the Application Modul’s Data Model: add EmpView under DeptView – based on the ViewLink just created

 

4. In the View Controller project:

Create a New JSF JSP page

Drag the AppModuleDataControl.DeptView collection to the new untitled.jspx page. Select Trees, ADF Tree from the dialog

In the Tree Binding Editor, define two rules:

The First Rule establishes the root-nodes for the tree, based on the DeptView Collection, with Dname for the Node Label and EmpView as the accessor for the first (and only) branch with childnodes.

 

The Second Rule introduces Emp nodes into the tree, using Ename for the Node Labels and having it’s own branch of additional Emp childnodes based on the EmpView branch rule accessor.

 

 

Now the application is ready and can be run. The result is shown at the top of this article. All nodes – regardless of whether they have children or not – are shown with the Folder icon. This will invite users to expand nodes that turn out to have no child nodes and is misleading as well as annoying.

Extending the ADF Framework with a smarter model

In the previous articles – referred in the introduction – I have explained what the reasoning was for the ADF development team to implement the current behavior: inspecting every nodes to find out from the ADF Model and underlying iterator whether any children exist for that node was deemed far too much of a performance load. We will see now how we can override this somewhat paternalistic attitude for situations where that load is not so heavy (small tables for example) or we have smarter way of finding out whether there are children or not.

This approach requires us to implement our own binding-class for trees and register it somehow with our application. Fortunately we can use subclassing of existing ADF classes to a large extend and end up writing very little code of our own.

The starting point is the ADFBindingFilter that is run at the very start of the application to perform certain initialization steps. Among these steps is the instantiation and registration of several factory classes that will be used by the ADF Framework to get implementations of binding classes. We will have to override the default ADFBindingFilter class to register our own extended factory.

The ADFBindingFilter is registered as Filter in the web.xml file; we will specify our own Filter instead (and develop that Filter class in just a moment):

    &lt;filter&gt;<br />        &lt;filter-name&gt;adfBindings&lt;/filter-name&gt;<br />        &lt;filter-class&gt;nl.amis.view.MyADFBindingFilter&lt;/filter-class&gt;<br />    &lt;/filter&gt;<br /><br />

The BindingFilter we need is largely the same as the ADFBindingFilter; we just need a small override to register our own BindingDefFactoryImpl instead of the hard-coded one shipped with ADF Faces: FacesBindingDefFactoryImpl

package nl.amis.view;<br />import oracle.adf.model.servlet.ADFBindingFilter;<br />import javax.servlet.FilterConfig;<br />import javax.servlet.ServletException;<br />import oracle.jbo.uicli.mom.JUMetaObjectManager;<br /><br />public class MyADFBindingFilter extends ADFBindingFilter  {<br />    public void init(FilterConfig filterConfig) throws ServletException {<br />      super.init(filterConfig);<br />      JUMetaObjectManager.setControlDefFactory(new MyCustomBindingDefFactoryImpl());<br />      JUMetaObjectManager.getJUMom().registerDefinitionFactory(<br />                 &quot;http://xmlns.oracle.com/adfm/uimodel&quot;,new MyCustomBindingDefFactoryImpl());<br />    }<br />}<br /><br />

 

The FactoryImpl we register – MyCustomBindingDefFactoryImpl – is our own class, that extends of course from one of the standard ADF Classes:

package nl.amis.view;<br /><br />import oracle.jbo.uicli.binding.JUBindingDefFactoryImpl;<br /><br />import oracle.adf.model.binding.DCDefBase;<br /><br />import oracle.adfinternal.view.faces.model.binding.FacesCtrlActionDef;<br />import oracle.adfinternal.view.faces.model.binding.FacesCtrlAttrsDef;<br />import oracle.adfinternal.view.faces.model.binding.FacesCtrlHierDef;<br />import oracle.adfinternal.view.faces.model.binding.FacesCtrlListBinding;<br />import oracle.adfinternal.view.faces.model.binding.FacesCtrlListDef;<br />import oracle.adfinternal.view.faces.model.binding.FacesCtrlRangeDef;<br /><br />import oracle.jbo.mom.xml.DefElement;<br />import oracle.jbo.uicli.mom.JUTags;<br />import nl.amis.view.MyFacesCtrlHierBinding;<br /><br />import oracle.adfinternal.view.faces.model.binding.MyFacesCtrlHierDef;<br /><br />public class MyCustomBindingDefFactoryImpl extends JUBindingDefFactoryImpl {<br /><br />    public DCDefBase createDefinition(DefElement element) {<br />        //copied from FacesBindingDefFactoryImpl<br />        String sElement = element.getElementName();<br />        if (sElement.equals(JUTags.PNAME_table)) {<br />            return new FacesCtrlRangeDef();<br />        } else if (sElement.equals(JUTags.PNAME_tree)) {<br />            // here is
 my new code!!!!<br />            ret
urn new MyFacesCtrlHierDef();<br />        } else if (sElement.equals(JUTags.PNAME_listOfValues) || <br />                   sElement.equals(JUTags.PNAME_list)) {<br />            FacesCtrlListDef def = new FacesCtrlListDef();<br />            def.setSubType(DCDefBase.PNAME_ListSingleSel);<br />            def.setControlBindingClassName(FacesCtrlListBinding.class.getName());<br />            //       def.setStaticList(false);<br />            return def;<br />        } else if (sElement.equals(JUTags.PNAME_attributeValues)) {<br />            return new FacesCtrlAttrsDef();<br />        } else if (sElement.equals(JUTags.PNAME_methodAction) || <br />                   sElement.equals(JUTags.PNAME_action)) {<br />            return new FacesCtrlActionDef();<br />        }<br />        return super.createDefinition(element);<br />    }<br />}<br /><br />&nbsp;

This class overrides just a single method: createDefinition. This method is invoked whenever the ADF Binding layer needs to instantiate a Model Binding component for use in a Page as instructed by the PageDefinition. My class contains some code copied from FacesBindingDefFactoryImpl with one change: the DCDefBase returned for a Tree element.

The implementation of the MyFacesCtrlHierDef is very simple: it only returns the correct – MyFacesCtrlHierBinding instead of FacesCtrlHierBinding – DCControlBinding I want to use for tree.

package oracle.adfinternal.view.faces.model.binding;<br />import oracle.adf.model.binding.DCBindingContainer;<br />import oracle.adf.model.binding.DCControlBinding;<br />import oracle.jbo.uicli.binding.JUCtrlHierDef;<br />import oracle.jbo.uicli.binding.JUIteratorBinding;<br /><br /><br />/**<br /> * This definition class provides instances of FacesCtrlHierBinding<br /> * for tree bindings.<br /> */<br />public final class MyFacesCtrlHierDef extends JUCtrlHierDef<br />{<br />  public MyFacesCtrlHierDef()<br />  {<br />  }<br /><br />  protected DCControlBinding createControlBindingInstance(Object control, DCBindingContainer formBnd)<br />  {<br />    return new MyFacesCtrlHierBinding(control, <br />                                    (JUIteratorBinding)getIterBinding(formBnd), <br />                                    getAttrNames(),<br />                                    getTypeBindings());<br />  }<br />} <br />

The real logic now finally can be written in the MyFacesCtrlHierBinding class – largely a copy from the (final!) class FacesCtrlHierBinding. I only show here the method I have changed after copying all code from FacesCtrlHierBinding:

    public boolean isContainer()<br />    {<br />      JUCtrlHierNodeBinding node = _getRowData();<br />      // LJ: test if there is binding for an attribute called IsContainer<br />      //     if there is one, we assume it will return a String with value N<br />      //     whenever a node is NOT a container - has no children.<br />      //     If such an attribute binding does not exist, we will fall back <br />      //     to default behavior, which is always return true;<br />      <br />      // to implement:<br />      // - add an attribute IsContainer (type String) to your ViewObject<br />      // - implement the getIsContainer() method in the ViewRowImpl class<br />      // - add the attribute to the PageDefinition (UI Model):<br />      //   ...<br />      //     &lt;Item Value=&quot;IsContainer&quot;/&gt;<br />      //   &lt;/AttrNames&gt;<br /><br />      if (node.getRow().getAttributeIndexOf(&quot;IsContainer&quot;) &gt; -1) {<br />        return (!&quot;N&quot;.equalsIgnoreCase((String)node.getAttribute(&quot;IsContainer&quot;)));<br />      }<br />      return true;<br />    }<br />&nbsp;

The isContainer method is called for every node to determine whether the Folder icon should be displayed for the node. The default implementation from the ADF team always returns true. My implementation does the following: it inspects the current Row to find out whether it contains an Attribute called IsContainer. If so, it will retrieve the value of that Attribute and check if it is N. Only if the value is available and it is equal to N will the method assume that the node has no children and should not be expandable and labeled with the folder icon.

To leverage this implementation of isContainer(), we have to do the following:

  1. Add an Attribute IsContainer to the ViewObject(s) underlying the tree
  2. Implement the getIsContainer method in the ViewRowImpl class(es) for these ViewObject(s) – make them return N whenever the node has no children
  3. Add the IsContainer attribute to the PageDefinition of the page that contains the tree

When this is in place, restart the application. If all still works as before, we probably have done the right things (or we have done nothing at all). Let’s now proof the pudding:

1. Add a new transient Attribute IsContainer to the EmpView ViewObject (type is String)

2. Generate the ViewObjectRowImpl for EmpView

3. Implement the getIsContainer method in the EmpViewRowImpl class:

    public String getIsContainer() {<br />        return getJob().equalsIgnoreCase(&quot;MANAGER&quot;)?&quot;Y&quot;:&quot;N&quot;;<br />    }<br /><br />

4. Add the IsContainer attribute to the PageDefinition file:

...<br />&lt;nodeDefinition DefName=&quot;model.EmpView&quot; id=&quot;EmpViewNode&quot;&gt;<br />  &lt;AttrNames&gt;<br />    &lt;Item Value=&quot;Ename&quot;/&gt;<br />    &lt;!-- the next line was added! --&gt;<br />    &lt;Item Value=&quot;IsContainer&quot;/&gt;<br />  &lt;/AttrNames&gt;<br />  &lt;Accessors&gt;<br />    &lt;Item Value=&quot;EmpView&quot;/&gt;<br />  &lt;/Accessors&gt;<br />...<br />

5. Run the application:

Here we see the desired result: any node (Employee) that specifies IsContainer equals N (all employees who are not a MANAGER) does not have the folder icon and the option to expand! 

To get rid of the Y and N values in the Employee node labels, we have to slightly enhance the JSPX page. We implement a somewhat sophisticated nodestamp facet:

&lt;f:facet name=&quot;nodeStamp&quot;&gt;<br />  &lt;af:switcher facetName=&quot;#{node.hierType.name}&quot;&gt;<br />    &lt;f:facet name=&quot;DeptViewNode&quot;&gt;<br />      &lt;af:outputText value=&quot;#{node.Dname}&quot;/&gt;<br />    &lt;/f:facet&gt;<br />    &lt;f:facet name=&quot;EmpViewNode&quot;&gt;<br />      &lt;af:outputText value=&quot;#{node.Ename}&quot;/&gt;<br />    &lt;/f:facet&gt;<br />  &lt;/af:switcher&gt;<br />&lt;/f:facet&gt;<br />

 

And beyond… 

Of course it should be fairly simple to implement different types of
logic, for example code that may not know in advanced whether a node
has children, but that will remember when the node has been expanded
and has been found to not have children.

Resources

Download the completed Hrm Application with extended Tree model: HrmTreeNoLeafNodeSolution.zip 

Share.

About Author

Lucas Jellema, active in IT (and with Oracle) since 1994. Oracle ACE Director for Fusion Middleware. Consultant, trainer and instructor on diverse areas including Oracle Database (SQL & PLSQL), Service Oriented Architecture, BPM, ADF, Java in various shapes and forms and many other things. Author of the Oracle Press book: Oracle SOA Suite 11g Handbook. Frequent presenter on conferences such as JavaOne, Oracle OpenWorld, ODTUG Kaleidoscope, Devoxx and OBUG. Presenter for Oracle University Celebrity specials.

7 Comments

  1. I am looking for the same solution in JDeveloper 11.1.2. Please can you suggest me something.
     
    Thanks in advance

  2. I like your postings where I can find the direct solution. Your postings helped me a lot so far but now im in need of implementing lazy loading for ADF Tree Is it possible ? If so please give me an idea or any referance implementaion.

    Thanks in advance …

  3. This worked in 10.1.3.1, but doesn’t work in 10.1.3.2. I’m not exactly sure why MyCustomBindingDefFactoryImpl ::createDefinition isn’t called for the tree anymore. (In fact, it’s only called for the pageDefintion.) Any ideas?

  4. A bit kludgy, but it works. Note that the class “MyFacesCtrlHierDef ” has to be part of package “oracle.adfinternal.view.faces.model.binding” to get around access restrictions. Also, there’s a small typo in isContainer (getRow is missing):

    return (!”N”.equalsIgnoreCase((String)node.getRow().getAttribute(“IsContainer”)));