For a project we are currently executing at AMIS, using the Oracle ADF, with ADF BC, Binding and Faces as well as JHeadstart 10.1.3 (currently in beta), the need arose to make use of Tree components in the web UI. A requirement for this tree was that it had to be mixed: nodes based on various ViewObjects. In this article, I will show how we were able to create such a tree in less than 5 minutes, using JHeadstart. We will also see what remaining issues there – apparently issues in ADF itself.
An introduction to the ADF Faces Tree Component can be read in this previous blog article: Getting started with ADF Faces in JDeveloper 10.1.3 – Using the Tree Component, comparison with MyFaces Tomahawk Tree2
The first steps for getting this application up and running are fairly trivial:
Preparing the Application and Model project
The first step is normal JHeadstart procedure. Our starting point is
the SCOTT schema (EMP and DEPT tables) and a JDeveloper 10.1.3 with
JHeadstart 10.1.3 (build 78) installation.
Create the Model Project
- Start JDeveloper
- Create
a new Application; choose the Web Application [JSF, ADF BC] Application
Template. Call the Application for example HRM. JDeveloper creates two
projects: Model and ViewController. - On the Model project,
select from the New Gallery the option Business Tier, ADF Business
Components, Business Components for Tables - Select or Create a Connection to the SCOTT schema
- Select
the tables EMP and DEPT to create Entity and Updateable ViewObjects
for; accept the default package and application module names - Define
primary keys for the EMP and DEPT EntityObjects: Select the Emp
EntityObject; select the EMPNO attribute, check the Primary Key
Checkbox, select the RowID attribute and uncheck the Primary Key
checkbox. Select the DEPT EntityObject, select the EMPNO attribute,
check the Primary Key Checkbox, select the RowID attribute and uncheck
the Primary Key checkbox. - Remove the RowID attribute from both ViewObjects
- Create a ViewLink from DeptView (source) to EmpView (target). The join condition links DeptView.Deptno to EmpView.Deptno
- Create a ViewLink from EmpView (source) to EmpView (target). The join condition links EmpView.Empno to EmpView.Mgr
- Add EmpView as child in the Application Module’s DataModel:
This
concludes the Model project. If you feel like testing it, you could run
the Test option from the Right Mouse Button Menu on the AppModule
Application Module.
Create and Generate the ViewController Project
Now we will generate a straightforward EMP and DEPT application
- From the RMB menu on the ViewController project, select the option Enable JHeadstart on this Project. Run and Finish the wizard.
- Again,
from the RMB menu on the ViewController project, select the option New
JHeadstart Application Definition. A wizard is run. Accept all defaults
and Finish. A Default JHeadstart Application Definition file is
created. Save All. - You will find the new
ApplicationDefinition file can be found under the node Resources in the
ViewController project. Select the option Run JHeadstart Application
Generator from the RMB menu on this file. The default application is
now generated. Press Save All. - Run the file EmpTable.jspx under ViewController\WebContent\WEB-INF\page to inspect the application.
Introduce the Tree Group to the Application
1. Open the JHeadstart Application Definition Editor
2. Create a New Group; call it HrmTree for example. Set the Layout-Style to Tree-Form. Select DeptView1 as DataCollection and as TreeDataCollection. Synchronize the Group to create Items for all Attributes. Set the Descriptor Item to Dname. Note: we have an opportunity here to distinguish between the DataCollection (ADF DataControl Iterator) to use for presenting the nodes in the tree and for creating the Edit Form for the selected node. We will not go into more details here.
3. Create a Detail Group under the HrmTree group for the Detail (Emp) Nodes. Set the Tree Data Collection – the ADF DataControl iterator for the detail Emp nodes under parent Dept and (manager) Emp nodes – to EmpView2 (referring to the detail ViewObject Usages in the AppModule’s DataModel). Set the Data Collection to EmpView1. Specify the tree-form layout style. Synchronize the Group and set the Descriptor Item to Ename.
4. That is it! Now generate the application using the JHeadstart Application Generator which will create the PageDefinitions for the ADF Binding Framework, the JSF JSP page and supporting elements like faces-config.xml and a resource bundle.
5. When generation is done, run the application:
If we open one of the Department Nodes and select a Child Employee node the page looks like:
Remaining Issues
One very annoying issue with the Tree is that regardless of whether a node has children, it will be displayed as an exandable node (in fact it stays this way even when expansion has revealed that there are no leaf-nodes under this node). See for example SMITH in the screenshot above. I had noticed this before in my previous analysis of the ADF Faces Tree Component and consultation with Steven Davelaar (Mr. JHeadstart) confirms that this is in fact a bug in ADF (apparently originally an intentional feature as inspecting all nodes for the existence of child nodes was deemed to performance expensive). Steven says it has now been recognized as a bug, and since it is hugely bothering him at his current project he is trying to put some pressure on the development team to have something done about it. It seems that the issue is not in the Tree Component itself, but in the ADF Binding Framework’s implementation of the TreeModel.
(modified 30th July 2006): A little digging reveals some more details:
The classes relevant to the way ADF (Binding) provides the TreeModel required by the ADF Faces Tree component are oracle.adfinternal.view.faces.model.binding.FacesCtrlHierBinding and its inner class FacesModel. The method isContainer that is invoked by the ADF Faces Tree renderer to determine which icon to display for a node is implemented as follows in this FacesModel class:
public boolean isContainer() { // the following is too expensive at the moment: bug 4597193 // JUCtrlHierNodeBinding node = _getRowData(); // return node.getChildren() != null; return true; }
Now my question would be: where is the configuration detail that links the FacesCtrlHierBinding class to the value=”#{bindings.HrmTreeTree.treeModel}” attribute on the <af:tree > component? If I know how to change that configuration setting, I should be able to use my own subclass that extends FacesCtrlHierBinding and does implement the isContainer() method, at least whenever I want it to. I will try to find the answer to this question.
OK, some more research revealed that class oracle.adfinternal.view.faces.model.binding.FacesBindingDefFactoryImpl is the one responsible for tying the <tree> component in the PageDefinition to the FacesCtrlHierDef() class:
public final class FacesBindingDefFactoryImpl extends JUBindingDefFactoryImpl { public DCDefBase createDefinition(DefElement element) { String sElement = element.getElementName(); ... else if(sElement.equals(JUTags.PNAME_tree)) { return new FacesCtrlHierDef(); }
Instead of a configurable mapping, it turns out to be a hard-coded reference to the FacesCtrlHierDef() class which in turns returns FacesCtrlHierBinding() instances. So the question now becomes: is the FacesBindingDefFactoryImpl class configured somewhere and if so, can it be replaced with my own Factory Class that can render different mappings for specific tags from the PageDefinition file? The next stage takes me to the oracle.jbo.uicli.mom.JUMetaObjectManager class. This class has the method getFactoryMap() that returns a Map that contains the name of the BindingDefFactory implementation class. This method will initiate the Factories oracle.jbo.uicli.binding.JUBindingDefFactoryImpl and DataControlDefinitionFactory if it cannot find the factory-definitions where it expects to find them. In my case it finds some settings using the call
com.sun.java.util.collections.HashMap factoryMap = (com.sun.java.util.collections.HashMap)findLoadedObject("ADFNameSpaceMap");
I have not been able to figure out so far where the values loaded by this method come from:
- key: http://xmlns.oracle.com/adfm/uimodel
- value: oracle.adfinternal.view.faces.model.binding.FacesBindingDefFactoryImpl
findLoadedObject is a method on the superclass oracle.jbo.mom.DefinitionManager. This class is located in the JDEV_HOME\BC4J\lib\adfm.jar file. I suppose this class is instantiated very early on in the application life cycle. However, where it gets its values from still eludes me.
Thanks to Steve Muench who just pointed me at his Not Yet Documented Examples – I think I now know the way to go. See: Example of Using Customized ADF Binding Class in a ADF/Struts Web Application
In short:
MyADFBindingFilter extends the base ADFBindingFilter and calls
JUMetaObjectManager.setControlDefFactory() to register a custom
control binding definition
factory. MyCustomBindingDefFactoryImpl extends the default
JUBindingDefFactoryImpl and overrides the createControlDef()
method to return a different binding definition class for Trees.
Trying out Steve’s suggestion to configure the web.xml with my own ADFBindingFilter that sets my own ControlDefFactory, I perceive the following: during the very early stages of the application, a call is made from ADFPhaseListener to register the ControlDefFactory all by itself:
/** * The definition factory used to create custom ADFm definition objects for * the Faces environment. */ static { JUMetaObjectManager.getJUMom().registerDefinitionFactory( "http://xmlns.oracle.com/adfm/uimodel", new oracle.adfinternal.view.faces.model.binding.FacesBindingDefFactoryImpl()); }
The first time the createDefinition() method on the JUBindingDefFactory is invoked, my own FactoryImpl is called. However, subsequent calls to oracle.jbo.uicli.mom.JUMetaObjectManager.getFactoryMap() seem to be returning the FacesBindingDefFactoryImpl() which in turn means my BindingDefFactory does not get to play anymore. Now when I add the same call made in this static block in ADFPhaseListener to my custom ADFBindingFilter, it seems like I get what I really want: a permanent replacement of the ControlDefFactory:
public class MyADFBindingFilter extends ADFBindingFilter { public void init(FilterConfig filterConfig) throws ServletException { super.init(filterConfig); JUMetaObjectManager.setControlDefFactory(new MyCustomBindingDefFactoryImpl()); JUMetaObjectManager.getJUMom().registerDefinitionFactory( "http://xmlns.oracle.com/adfm/uimodel",new MyCustomBindingDefFactoryImpl()); }
The next step obviously is to implement my own extension to FacesCtrlHierBinding, one that works with my extension to the FacesModel class and that has a method isContainer with a proper implementation. More on this later.
Conclusion
Developing a fairly complex mixed node tree component with Node specific Edit Form is extremely simple with ADF Faces, ADF Business Components and JHeadstart 10.1.3. It is a construction that will feature quite prominently in the application we are currently developing, so the productivity gains we are getting are very substantial indeed.
Added some more analysis results on how the ADF Model/Binding stuff implements the TreeModel required by the ADF Tree component – and how it does not implement the isContainer() method.