One of the key user interface components for our recently started ADF Faces/JHeadstart project is a Tree. The data we are dealing with has a nested structured with items of one type (Catalog) containing items of another type (Catalog Items) that in turn can be nested across multiple levels (Catalog Item Family, Catalog Item Catagory, Catalog Item, Catalog Sub Item etc.). A representation in a Tree is the natural fit for this data structure. Using JHeadstart we are able to generate most of our layout, navigation and per-page interaction. However, the generated application lacks one rather important feature: there is no option to create a child node if there is not already at least one. If all we have is a Catalog, and we want to add the very first child Catalog Item, the display has no button to do so.
The second issue has to do with defaults: if we create a new Catalog Item – as a sibling to an existing Item under a certain node – we want the new Item to automatically appear under the same parent Item and root Catalog. We need to set Default Values for the foreign keys to Parent and Root (Ancestor). That too turns out not to be trivial. This article describes a way to resolve both issues – in terms of an HRM Tree – on DEPT and EMP. Steven Davelaar – lead architect for JHeadstart and speaker in our next AMIS Query on 14th of September – tells me that the desired behavior unfortunately missed the cut off for the JHeadstart 10.1.3 Production Release Candidate but will be added later this year.
after pressing the Create Subordinate button, a page appears where a new Employee can be created. The Manager defaults to ALLEN and the Department defaults to SALES:
After saving the new data, the tree looks like this:
We will first create the application, generate the tree layout and then start worrying about the desired behavior in terms of proper default values and creation of child nodes. See for a more step-by-step explanation the article Building an ADF Faces Tree Component with mixed (DEPT and EMP) nodes in 5 minutes using JHeadstart 10.1.3.
Create the application with the HrmTree
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 new ViewObject: ManagersView. It should be a near copy of the EmpView, with a specific SQL Where Clause: JOB = ‘MANAGER’
- Create a ViewLink from DeptView (source) to ManagersView (target).
The join condition links DeptView.Deptno to ManagersView.Deptno - Create a ViewLink from ManagersView (source) to EmpView (target).
The join condition links ManagersView.Empno to EmpView.Mgr - Create a ViewLink from EmpView (source) to EmpView (target). The join condition links EmpView.Empno to EmpView.Mgr
- In the Application Module’s DataModel, ensure that all three ViewObjects are individually available at the highest level. Also create a nested structure, starting with a DeptView usage with nested ManagersView usage followed by a nested EmpView usage:
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 (Manager)
Nodes. Set the Tree Data Collection – the ADF DataControl iterator for
the detail Manager nodes under parent Dept – to ManagersView1 (referring to the detail ViewObject Usage under DeptView1 in the AppModule’s
DataModel). Set the Data Collection to ManagersView2 – the not-nested, highest level ManagersView object usage in the Data Model. Specify the tree-form
layout style. Synchronize the Group and set the Descriptor Item to
Ename.
4. Create a Detail Group under the Managers group for the Detail/Subordinate (Employee)
Nodes. Set the Tree Data Collection – the ADF DataControl iterator for
the detail Manager nodes under parent Manager – to EmpView2
(referring to the detail ViewObject Usage under ManagersView1 in the
AppModule’s
DataModel). Set the Data Collection to EmpView1 – the not-nested,
highest level EmpView object usage in the Data Model. Specify the
tree-form
layout style. Synchronize the Group and set the Descriptor Item to
Ename.
5.
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.
6. When generation is done, run the application:
Adding the option to create child-nodes
At this point you will not have any default values, nor will there be Create <ChildNode> buttons on the Edit Forms. If you have a Department without Manager, you cannot create a Manager for it. If you have a Manager without subordinate employees – a sad sight to see – there is no way to create subordinates for this manager. That is what we will add in the next section of this article.
The Edit Manager page (Managers.jspx) contains a button Create Manager. If only we would have such a button on the Edit Department page! Well, it is simple to copy and paste that button from Managers.jspx to HrmTree.jspx. However, this button has its actionListener attribute set to #{bindings.CreateManagers.execute}. For the HrmTree.jspx page, #{bindings} refers to HrmTreePageDef(.xml), which is not correct since the CreateManagers operation is one on the ManagersPageDef page definition. So we have to replace the actionListener for this button with one referring to #{data.ManagersPageDef}:
<af:commandButton actionListener="#{data.ManagersPageDef.CreateManagers.execute}"
action="CreateManagers"
textAndAccessKey="#{nls['NEW_BUTTON_LABEL_MANAGERS']}"
rendered="#{!createModes.CreateManagers }"
immediate="true"
onclick="return alertForChanges();">
<af:setActionListener from="#{createEmp}"
to="#{HrmTreeTree.operation}"/>
<f:actionListener type="oracle.jheadstart.controller.jsf.listener.DoRollbackActionListener"/>
<af:resetActionListener/>
</af:commandButton>
The same can be done with the Create Employee button on the Employees.jspx page that we can copy to the Managers.jspx page:
<af:commandButton actionListener="#{data.EmployeesPageDef.CreateEmployees.execute}"
<af:setActionListener from="#{'createSubordinate'}"
action="CreateSubordinate"
textAndAccessKey="Create Subordinate"
rendered="#{!createModes.CreateManagers }"
immediate="true"
onclick="return alertForChanges();">
to="#{HrmTreeTree.operation}"/><f:actionListener type="oracle.jheadstart.controller.jsf.listener.DoRollbackActionListener"/>
<af:resetActionListener/>
</af:commandButton>
Again we have to change the actionListener to not refer to the bindings object – which is still the ManagersPageDef when the actionListener is executed – but instead to data.EmployeesPageDef. I have also changed the textAndAccessKey – not yet to a resource bundle value but a hard coded value – to Create Subordinate. Finally we have added the setActionListener element, to set the proper value for the operation property in the HrmTreeTree bean.
Finally we want the Employees.jspx page to contain an additional button, also labeled Create Subordinate. The distinction between the New Employee and the Create Subordinate buttons will be that the first creates a new employee with the same Department and Manager as the currently selected Employee, while the latter creates a new employee with the same Department and the currently selected Employee as manager. This button is also copied from Employees.jspx and then customized:
<af:commandButton actionListener="#{bindings.CreateEmployees.execute}"
action="CreateSubordinate"
textAndAccessKey="Create Subordinate"
rendered="#{!createModes.CreateEmployees }"
immediate="true"
onclick="return alertForChanges();">
<af:setActionListener from="#{'createSubordinate'}"
to="#{HrmTreeTree.operation}"/>
<f:actionListener type="oracle.jheadstart.controller.jsf.listener.DoRollbackActionListener"/>
<af:resetActionListener/>
</af:commandButton>
You
will notice that I have added the setActionListener. This element ensures that before any of the ActionListeners or the Action method are invoked, the value ‘createSubordinate’ is set in the operation property on HrmTreeTree bean. In the default values for Mgr we can refer to this operation value – as we will see in a moment.
We
need one further piece to get the show on the road: we must ensure that
the action values we have specified for these buttons – CreateManagers
and CreateSubordinate – are backed by navigation cases in the
faces-config.xml file. We add the following cases:
<navigation-rule>
<from-view-id>*</from-view-id>
<navigation-case>
<from-outcome>CreateSubordinate</from-outcome>
<to-view-id>/WEB-INF/page/Employees.jspx</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>CreateManagers</from-outcome>
<to-view-id>/WEB-INF/page/Managers.jspx</to-view-id>
</navigation-case>
...
Note
that using the JHeadstart 10.1.3 Templating Framework, these changes
can easily be incorporated in the generation process so they do not
prevent the generation of the application.
Setting proper default values for new Managers and Employees
If
we create a new Manager under a currently selected Department, we want
to set the Deptno for that new Manager to the Deptno of the currently
selected Department Node. When we create a new Employee under a Manager
or other Employee, we want to default both Deptno and Mgr to
respectively the Deptno and Empno of the currently selected Manager or
Employee. The issue here is: the currently selected tree node.
JHeadstart uses a tree bean – configured in WEB-INF\HrmTree-beans.xml
and based on oracle.jheadstart.controller.jsf.bean.TreeBean.
This object is called whenever a node in the tree is selected. Through
its getSelectedNode() method, one can always access the currently
selected node in the tree. However: when we get to the New Manager or
New Employee page and mode, getSelectedNode() suddenly returns null. So
instead of always being able to set default values using an expression
like #{HrmTreeTree.selectedNode.row.attributeValues[7]}
– which provides a way to get at specific values in the data row
underlying the currently selected tree node – we have to find another
way to remember the essential attribute values when we go to the New
Manager page.
What I have decided to do – and perhaps there are
much better ways of doing this but at least I got this one to work – is
extend the TreeBean class. I have created my own nl.amis.view.HrmTreeBean
class. In the file HrmTree-beans.xml, where JHeadstart has generated
the configuration of the generic TreeBean class, I make a modification (post generation change!) to ensure my own class will be used:
<managed-bean>
<managed-bean-name>HrmTreeTree</managed-bean-name>
<managed-bean-class>nl.amis.view.HrmTreeBean</managed-bean-class>
<!-- Scope must be session so we maintain tree expand/collapse state accross requests! -->
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>showExpanded</property-name>
<value>false</value>
</managed-property>
</managed-bean>
The
HrmTreeBean class overrides the setSelectedNode() method from the
superclass and retrieves the necessary information to derive default
values later on:
public void setSelectedNode(Object selectedNode) {
super.setSelectedNode(selectedNode);
JUCtrlHierNodeBinding node = (JUCtrlHierNodeBinding)selectedNode;
setDeptno((oracle.jbo.domain.Number)node.getAttribute("Deptno"));
// if the iterator is either for emp or for mgr
// then we can extract the Empno and Mgr attributes
if (((node.getIteratorBinding().getName().toLowerCase().indexOf("emp")) >
-1) || (node.getIteratorBinding().getName().toLowerCase().indexOf("manager") >
-1)) {
setEmpno((oracle.jbo.domain.Number)node.getAttribute("Empno"));
setMgr((oracle.jbo.domain.Number)node.getAttribute("Mgr"));
}
else {
setEmpno(new oracle.jbo.domain.Number());
setMgr(new oracle.jbo.domain.Number());
}
}
Note that I have also added properties operation, deptno, empno and mgr to this bean and generated accessors for these properties.
What
I have achieved now is that whenever a node is selected in the HrmTree
– be it a Department, a Manager or an Employee – the HrmTreeBean will
remember the essentials for the selected node. The default values we
want to define for new managers and new employees can now be specified
in terms of this bean.
We define the following Default Display values:
Group Manager
Attribute Deptno: #{HrmTreeTree.deptno}
Attribute Job: MANAGER
Group Employees
Attribute Deptno: #{HrmTreeTree.deptno}
Attribute Mgr: #{HrmTreeTree.operation==’createSubordinate’? HrmTreeTree.empno:HrmTreeTree.mgr}
The Mgr attribute gets a default value depending on the operation we are trying to execute. If we are creating a subordinate, the operation is called createSubordinate (in the operation property of the HrmTreeTree bean as set by the setActionListener elements on the Create Subordinate buttons on both Managers and Employees pages) and the default value should be the empno of the currently selected node, as provided through the empno property on the HrmTreeTree bean. If on the other hand the operation is another one – createEmployee for example – the default value for Mgr is to be derived from the Mgr property from the current node’s Mgr property – again as available on the HrmTreeTree bean.
We can hide these attributes or disable them: the
user does not need to be bothered with them as they should not be
updated and their values should be clear from the node context.
Resources
Download the HRM Tree application (JDeveloper/JHeadstart 10.1.3 application): JHeadstartHrmTreeSubordinates.zip
The Initial now Improved upon Approach – do not use the approach outlined below anymore!!!
This article initially was posted on 3th August 2006 – with the approach detailed below. However, on August 7th, the article was updated – so you are reading the refreshed version now – with an approach that turns out to be much leaner. I have included the following section for comparison – I do not recommend using it anymore, as the approach based on the setActionListener is much more elegant.
<af:commandButton actionListener="#{HrmTreeTree.createEmployee}"
action="CreateSubordinate"
textAndAccessKey="Create Subordinate"
rendered="#{!createModes.CreateEmployees }"
immediate="true"
onclick="return alertForChanges();">
<f:actionListener type="oracle.jheadstart.controller.jsf.listener.DoRollbackActionListener"/>
<af:resetActionListener/>
</af:commandButton>
You will notice that I have changed the actionListener. The reason for this is explained in the next section, on default value. For now if we run the application, this last button will not work, but the others will. That means we now have a way to create Managers for Departments that currently have no managers and Subordinates for Managers that currently have none. In the next section we will ensure that these newly created Managers and Subordinates refer to the proper Department and Manager.
We need one further piece to get the show on the road: we must ensure that the action values we have specified for these buttons – CreateManagers and CreateSubordinate – are backed by navigation cases in the faces-config.xml file. We add the following cases:
<navigation-rule>
<from-view-id>*</from-view-id>
<navigation-case>
<from-outcome>CreateSubordinate</from-outcome>
<to-view-id>/WEB-INF/page/Employees.jspx</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>CreateManagers</from-outcome>
<to-view-id>/WEB-INF/page/Managers.jspx</to-view-id>
</navigation-case>
...
Note that using the JHeadstart 10.1.3 Templating Framework, these changes can easily be incorporated in the generation process so they do not prevent the generation of the application.
Setting proper default values for new Managers and Employees
If we create a new Manager under a currently selected Department, we want to set the Deptno for that new Manager to the Deptno of the currently selected Department Node. When we create a new Employee under a Manager or other Employee, we want to default both Deptno and Mgr to respectively the Deptno and Empno of the currently selected Manager or Employee. The issue here is: the currently selected tree node. JHeadstart uses a tree bean – configured in WEB-INF\HrmTree-beans.xml and based on oracle.jheadstart.controller.jsf.bean.TreeBean. This object is called whenever a node in the tree is selected. Through its getSelectedNode() method, one can always access the currently selected node in the tree. However: when we get to the New Manager or New Employee page and mode, getSelectedNode() suddenly returns null. So instead of always being able to set default values using an expression like #{HrmTreeTree.selectedNode.row.attributeValues[7]} – which provides a way to get at specific values in the data row underlying the currently selected tree node – we have to find another way to remember the essential attribute values when we go to the New Manager page.
What I have decided to do – and perhaps there are much better ways of doing this but at least I got this one to work – is extend the TreeBean class. I have created my own nl.amis.view.HrmTreeBean class. In the file HrmTree-beans.xml, where JHeadstart has generated the configuration of the generic TreeBean class, I make a modification (post generation change!) to ensure my own class will be used:
<managed-bean>
<managed-bean-name>HrmTreeTree</managed-bean-name>
<managed-bean-class>nl.amis.view.HrmTreeBean</managed-bean-class>
<!-- Scope must be session so we maintain tree expand/collapse state accross requests! -->
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>showExpanded</property-name>
<value>false</value>
</managed-property>
</managed-bean>
The HrmTreeBean class overrides the setSelectedNode() method from the superclass and retrieves the necessary information to derive default values later on:
public void setSelectedNode(Object selectedNode) {
super.setSelectedNode(selectedNode);
JUCtrlHierNodeBinding node = (JUCtrlHierNodeBinding)selectedNode;
// alternatively:
// if (node.getHierTypeBinding().getName().equals
// ("TreeDeptNode"))
setDeptno((oracle.jbo.domain.Number)node.getAttribute("Deptno"));
// if emp based iterator, use Empno for Empno
if ((node.getIteratorBinding().getName().toLowerCase().indexOf("emp")) > -1)
setEmpno((oracle.jbo.domain.Number)node.getAttribute("Empno"));
else
setEmpno(new oracle.jbo.domain.Number());
if ((node.getIteratorBinding().getName().toLowerCase().indexOf("emp")) > -1)
setMgr((oracle.jbo.domain.Number)node.getAttribute("Mgr"));
// if manager iterator (we come from Manager page and potentially go create a subordinate)
else if (node.getIteratorBinding().getName().toLowerCase().indexOf("manager") > -1)
setMgr((oracle.jbo.domain.Number)node.getAttribute("Empno"));
else
setMgr(new oracle.jbo.domain.Number());
}
Note that I have also added properties deptno, empno and mgr to this bean and generated accessors for these properties.
What I have achieved now is that whenever a node is selected in the HrmTree – be it a Department, a Manager or an Employee – the HrmTreeBean will remember the essentials for the selected node. The default values we want to define for new managers and new employees can now be specified in terms of this bean.
We define the following Default Display values:
Group Manager
Attribute Deptno: #{HrmTreeTree.deptno}
Attribute Job: MANAGER
Group Employees
Attribute Deptno: #{HrmTreeTree.deptno}
Attribute Mgr: #{HrmTreeTree.mgr}
We can hide these attributes or disable them: the user does not need to be bothered with them as they should not be updated and their values should be clear from the node context.
(originally I had hoped to specify the Default Display Value using the #{JhsActionOutcome} expression as I know that JHeadstart sets this value. I had hoped an expression like #{JhsActionOutcome !=’CreateSubordinate’ ? HrmTreeTree.mgr : HrmTreeTree.empno} would have sufficed, taking most of the logic out of the HrmTreeBean. However, at the time the default values are evaluated, the JhsActionOutcome has not yet been set so that did not fly).
There is one last thing we have to take care of – and we come here to the reason for the modified actionListener. Somewhere, before or while the default values are applied, do we need to know whether we are going to create a new employee as subordinate of the current employee or as sibling (under the same manager). We can tell this from the button that has been pressed. To make the distinction – on time – between the two buttons New Employee and Create Subordinate, I have the latter button invoke a custom actionListener. This actionListener has to do a few things:
- set the Mgr property in the HrmTreeBean to the Empno of the current Employee instead of the Mgr of the current employee as is done in HrmTreeBean.setSelectedNode() (and we only invoke this actionListener when the create subordinate button has been pressed and we want to set the manager for the new employee to be the current employee
- invoke the original data.EmployeesPageDef.CreateEmployees action (the one originally used as actionListener in the action button)
- perform the additional processing normally done in JhsPageLifeCycle.onCreate() – specifically applying the default values (I am not too be happy with this last step, but it seems unavoidable).
Note that we can combine the last two steps by using the createNewRow() method on the DefaultValuesBean.
The createEmployee ActionListener method in the HrmTreeBean now is defined like this:
public void createEmployee(ActionEvent e) {
FacesContext fctx = FacesContext.getCurrentInstance();
Application fapp = fctx.getApplication();
//get the pageDef reference to the binding container
//the "bindings" object is set by the ADF Filter and
//is always be there
JUFormBinding adfbc =
(JUFormBinding)fapp.createValueBinding("#{data.EmployeesPageDef}").getValue(fctx);
DCIteratorBinding iter =
adfbc.findIteratorBinding("EmployeesIterator");
Row rw = iter.getCurrentRow();
oracle.jbo.domain.Number empno =
(oracle.jbo.domain.Number)rw.getAttribute("Empno");
Object hrmTreebean =
JsfUtils.getInstance().getApplication().getVariableResolver().resolveVariable
(fctx, "HrmTreeTree");
// because we know we come from an employee
// after having pressed the Create Subordinate button, we should get
// the current employee's empno and use that as Mgr for the new Employee!
((HrmTreeBean)hrmTreebean).setMgr(empno);
// now find the default values bean and have it create a new row with defaults applied to it:
// default values bean must be named after the create binding, suffixed with "DefaultValues"
String beanName = "CreateEmployees" + DEFAULT_VALUES_BEAN_SUFFIX;
Object bean =
JsfUtils.getInstance().getApplication().getVariableResolver().resolveVariable
(fctx, beanName);
if (bean != null && bean instanceof DefaultValuesBean) {
((DefaultValuesBean)bean).createNewRow();
}
getCreateModesBean().put("CreateEmployees",Boolean.TRUE);
}
public Map getCreateModesBean() {
FacesContext context = JsfUtils.getInstance().getFacesContext();
Map bean =
(Map)JsfUtils.getInstance().getApplication().getVariableResolver().resolveVariable(context,
CREATE_MODES_BEAN);
if (bean == null) {
return new HashMap();
}
return bean;
}
Note: a more conservative approach – not using the createNewRow() on the DefaultValuesBean – would first invoke the CreateEmployees action binding and the the applyDefaultValues method. I presume the above approach is valid but I want to check with the JHeadstart team to be certain:
...
List bindings = adfbc.getControlBindings();
DCControlBinding ctrlBinding = adfbc.findCtrlBinding("CreateEmployees");
if (ctrlBinding instanceof JUCtrlActionBinding) {
JUCtrlActionBinding ctrlActionBinding = (JUCtrlActionBinding)ctrlBinding;
ctrlActionBinding.invoke();
// apply default values - since the onCreate in the JhsPageLifeCycle class is now by-passed
{
// check for default values bean, if exists, call applyDefaultValues on the bean
// derfault values bean must be named after the create binding, suffixed with "DefaultValues"
FacesContext context = JsfUtils.getInstance().getFacesContext();
String beanName =
ctrlActionBinding.getName() + DEFAULT_VALUES_BEAN_SUFFIX;
Object bean =
JsfUtils.getInstance().getApplication().getVariableResolver().resolveVariable(context, beanName);
if (bean != null && bean instanceof DefaultValuesBean) {
((DefaultValuesBean)bean).applyDefaultValues();
}
...
Resources
Download the HRM Tree application (JDeveloper/JHeadstart 10.1.3 application): HrmTreeWithCreateSubordinates.zip.
Instead of using a whole new ActionListener in order to distinguish between two different buttons, could we not use the af:setActionListener component that allows us to set a value before navigating? Like for example in trees for the command link that are used to display clickable nodes? Something like: setActionListener from="CreateSubordinate" to="#{HrmTreeTree.operation}"