dinsdag 1 november 2011

How to render different pages for each tree node

Technology: ADF11g
Developed in: JDeveloper 11.1.2.1.0
Browsers tested: Internet Explorer 8.0.7601.17514 and Firefox 7.0.1
Used database schema: HR
Used tables: EMPLOYEES, DEPARTMENTS, LOCATIONS and COUNTRIES


Summary



I received the question to help out how to display different pages for each child node that is clicked in an af:tree. In this blog a solution is provided for this question.

I choose for a solution that uses a dynamic region which is refreshed every time a node is selected in the tree.

The tree I created displays all departments and the subtree all employees for that department, if a department is selected I render in the region a page contianing 'nothing here'. If a employee underneath a department is selected and the department has a manager, the information of the manager is displayed, and if the department does not have a manager its location details are displayed.

For this blog I updated some data in the database: Employee ID 116 belongs to department 130.

update employees set department_id = 130 where employee_id = 116;

Now department Corporate Tax has an employee but no manager so if the employee is selected the location details are displayed.

Model



For this blog the EMPLOYEES, DEPARTMENTS, LOCATIONS and COUNTRIES tables of the HR schema are used.

Entities



Create the following entities:
Entity name Based on table of HR schema Customizations made
Employee EMPLOYEES None
Department DEPARTMENTS None
Country COUNTRIES None
Locations LOCATIONS None


Two associations have been created:
  • Between Department and Employee
  • Between Country and Location

View objects



Create the following view objects and create a view object and view row class for all of the view objects:
View object name Based on entities Customizations made
DepartmentsView Department None
EmployeesView Employee None
LocationsView Location and Country Bind variable b_id of type Integer, the where clause is extended with Location.LOCATION_ID = :b_id
ManagersView Employee Bind variable b_id of type Integer, the where clause is extended with Employee.EMPLOYEE_ID = :b_id


One view link is created:
  • Between DepartmentsView and EmployeesView using the created association

Application module



An application module is created with name HrAppModule which exposes:
  • DepartmentsView
    • EmployeesView (using the created view link)
  • LocationsView
  • ManagersView
Generate the application module java class.

ViewController



The model layer is finished so we can start designing the pages.

In the unbounded taskflow the main page is created this page will contain the tree and a region, in this region different bounded task flows can be rendered.

Unbounded taskflow



This taskflow contains the main page (jsf) and defines two different beans.

Tree.jsf



The main page is called Tree.jsf in this page I created a panelGroupLayout with horizontal layout. The first component in this panelGroupLayout is the tree.

The tree is created by drag and drop the DepartmentsView and choosing Tree - ADF tree.




In the Edit Tree Binding dialog that pops up add (with the green plus sign) the second level to display the employees. For the departments I set the DepartmentName as display attribute and for the Employees view the FirstName and LastName.



In the created af:tree tag add a selectionListener:

<af:tree value="#{bindings.DepartmenstView.treeModel}"

var="node"

selectionListener="#{hrTreeBean.selectionListener}"

rowSelection="single"

id="t1">

This selection listener will be implemented in a custom managed bean class.

In the unbounded task flow define the managed bean:
Managed bean property Value
Name hrTreeBean
Class nl.capgemini.marianneHorsch.adfTree.view.beans.TreeBean
Scope Request


In the implementation of the bean we define the selectionListener method, in this method we check:
  • Is only a department selected
  • Has the selected department (or if a employee is selected the department the employee is in) a manager.
In the first phase we only store and print out this information.

package nl.capgemini.marianneHorsch.adfTree.view.beans;



import java.util.Iterator;

import java.util.List;

import javax.faces.context.FacesContext;

import nl.capgemini.marianneHorsch.adfTree.model.services.HrAppModuleImpl;

import nl.capgemini.marianneHorsch.adfTree.model.views.DepartmenstViewRowImpl;

import nl.capgemini.marianneHorsch.adfTree.model.views.EmployeesViewRowImpl;

import oracle.adf.model.binding.DCBindingContainer;

import oracle.adf.model.binding.DCDataControl;

import oracle.binding.BindingContainer;

import oracle.jbo.Key;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.apache.myfaces.trinidad.event.SelectionEvent;



public class TreeBean {

private static final Log log = LogFactory.getLog(TreeBean.class);



public TreeBean() {

super();

}



public void selectionListener(SelectionEvent selectionEvent) {

Key departmentKey = null;

Key employeeKey = null;

if (selectionEvent.getAddedSet() != null) {

Iterator iter = selectionEvent.getAddedSet().iterator();

while (iter.hasNext()) {

List objList = (List)iter.next();

if (objList != null && !objList.isEmpty()) {

Object objKey = objList.get(0);

if (objKey instanceof Key) {

departmentKey = (Key)objKey;

}

}

if (objList.size() > 1) {

Object objKey = objList.get(1);

if (objKey != null && objKey instanceof Key) {

employeeKey = (Key)objKey;

}

}

log.debug("Department key: " + (departmentKey == null ? "" : departmentKey));

log.debug("Employee key: " + (employeeKey == null ? "" : employeeKey));

}

}



if (departmentKey != null) {

DepartmenstViewRowImpl row =

(DepartmenstViewRowImpl)getService().getDepartmenstView().getRow(departmentKey);

getService().getDepartmenstView().setCurrentRow(row);

}

if (employeeKey != null) {

EmployeesViewRowImpl row =

(EmployeesViewRowImpl)getService().getEmployeesView().getRow(employeeKey);

getService().getEmployeesView().setCurrentRow(row);

}



if (employeeKey == null) {

log.debug("No employee selected.");

} else {

DepartmenstViewRowImpl row =

(DepartmenstViewRowImpl)getService().getDepartmenstView().getCurrentRow();

if (row.getManagerId() == null) {

log.debug("NO manager.");

} else {

log.debug("There is a manager.");

}

}

}



private HrAppModuleImpl getService() {

DCBindingContainer bc =

(DCBindingContainer)FacesContext.getCurrentInstance().getApplication().evaluateExpressionGet(FacesContext.getCurrentInstance(), "#{bindings}", BindingContainer.class);

if (bc == null) {

return null;

}

DCDataControl dc = bc.findDataControl("HrAppModuleDataControl");

if (dc == null) {

return null;

}

return (HrAppModuleImpl)dc.getDataProvider();

}

}

If we run this application we see the tree and logging about what we have selected.

In the example I distinguish 3 situations:
  • If a department is selected I render a taskflow displaying nothing.
  • If a employee is selected that belongs to a department that has a manager the manager is displayed.
  • If a employee is selected that belongs to a department that does not have a manager the location of the department is displayed.

Empty flow



The empty flow is a bounded taskflow with a page fragment that only has an output text saying Nothing here.

The taskflow shares the data control with the calling taskflow.

Manager flow



The manager flow is a bounded taskflow with a page fragment that only contains a simple read only form for the ManagersView (by drag and drop from the data controls palette).

The taskflow shares the data control with the calling taskflow.

Location flow



The location flow is a bounded taskflow with a page fragment that only contains a simple read only form for the LocationsView (by drag and drop from the data controls palette).

The taskflow shares the data control with the calling taskflow.

Now the region can be added to the Tree.jsf. Drag and drop the EmptyFlow from the project navigator onto the Tree.jsf (underneath the af:tree tag). Choose Dynamic Region.


A popup appears where a bean can be selected use the green plus to create a new Bean leave the input parameter map empty. This new bean should also be configured in the unbounded taskflow:
Managed bean property Value
Name regionBean
Class nl.capgemini.marianneHorsch.adfTree.view.beans.HrRegionBean
Scope PageFlow


In the implementation of the bean class define all 3 bounded taskflows and creates methods to set the taskflow as being the current to display:

package nl.capgemini.marianneHorsch.adfTree.view.beans;



import oracle.adf.controller.TaskFlowId;



public class HrRegionBean {

private String taskFlowId = "/WEB-INF/EmptyFlow.xml#EmptyFlow";

private static final String EMPTY_TF = "/WEB-INF/EmptyFlow.xml#EmptyFlow";

private static final String LOCATION_TF = "/WEB-INF/LocationFlow.xml#LocationFlow";

private static final String MANAGER_TF = "/WEB-INF/ManagerFlow.xml#ManagerFlow";



public HrRegionBean() {

}



public TaskFlowId getDynamicTaskFlowId() {

return TaskFlowId.parse(taskFlowId);

}



public void startEmpty() {

taskFlowId = EMPTY_TF;

}

public void startLocation() {

taskFlowId = LOCATION_TF;

}

public void startManager() {

taskFlowId = MANAGER_TF;

}

}

The last part that is missing know is to start the taskflow when a node is started, before starting it the correct row is queried by binding the bind variable for the value in the departments row.

At the end of the selectionListener method of the TreeBean add:

HrRegionBean bean = (HrRegionBean) getBeanInstance("#{pageFlowScope.regionBean}");

if (employeeKey == null) {

bean.startEmpty();

} else {

DepartmenstViewRowImpl row =

(DepartmenstViewRowImpl)getService().getDepartmenstView().getCurrentRow();

if (row.getManagerId() == null) {

getService().getLocationsView().setb_id(row.getLocationId());

getService().getLocationsView().executeQuery();

getService().getLocationsView().setCurrentRow(getService().getLocationsView().first());

bean.startLocation();

} else {

getService().getManagersView().setb_id(row.getManagerId());

getService().getManagersView().executeQuery();

getService().getManagersView().setCurrentRow(getService().getLocationsView().first());

bean.startManager();

}

}

And the new method to retrieve the current HrRegionBean instance:

private Object getBeanInstance(String expression) {

FacesContext fc = FacesContext.getCurrentInstance();

ELContext elctx = fc.getELContext();

ExpressionFactory elFactory = fc.getApplication().getExpressionFactory();

return elFactory.createValueExpression(elctx, expression, Object.class).getValue(elctx);

}

When we run this we see:
  • On page open:
  • When we open Marketing and select Michael Hartstein:
  • And when we open Corporate Tax and select Shelli Baida:
  • And select IT:

maandag 5 september 2011

How to create a linking analysis overview

Technology: ADF11g
Developed in: JDeveloper 11.1.1.3.0
Browsers tested: Firefox 6.0.1 and Internet explorer 8 (8.0.6001.18702 64 bit edition)


Summary



I received a question is it possible to create a representation like shown below in ADF? In this blog a description how to solve this.



Database



In a database a table tst_cases is created:
Column name Datatype Mandatory Constraint(s)
ID Number(10,0) Yes Primary key
Name Varchar2(100) Yes Unique key
PARENT_CSE_ID Number(10,0) No Foreign key to ID


It's filled with content:
ID NAME PARENT_CSE_ID
1 Case 1
10 Case 10 1
20 Case 20 1
30 Case 30 1
101 Case 101 10
102 Case 102 10
201 Case 201 20
202 Case 202 20
203 Case 203 20



Model layer



Create with the Business Components for Tables an entity for the table (name Case), a view object for the entity (name CasesView) and an application module (name TestAppModule). An association and view link will be created as well.

Create another view object with name RootCasesView that uses the Case enity object but with where clause PARENT_CSE_ID IS NULL. Create a view link from the RootCasesView to the CasesViwe (using the Case_ParentCase_FK association):



View controller



  1. Create a page (HierachyViewer.jspx) and add it to the adfc-config.
  2. Drag and drop from the data controls palette the RootCasesView as hierarchy viewer:


  3. Choose radial:


  4. Select in hierarchy all 3 levels (and accept all other default values):


If this created page is run the page opens like this:



You can click on the + sign underneath the Case 10 box and then the page looks like this:



I would like to display all children on page opening. To achieve this you can just set the displayLevelsChildren property on the dvt:hierarchyViewer tag to 100 then it will display 100 children (if they exist). However I choose to set this property to the max number of levels that are currently present in the database table.

Determine the max number of levels



To determine the max number of levels 2 functions are created (implementation of the functions are found at the end of the blog):
  1. Get_levels_to_root (i_n_cse_id IN tst_cases.id%TYPE); this function returns the number of levels between i_n_cse_id and the root tst_cases (with parent_cse_id null)
  2. Get_max_number_of_children; this functions loops over all tst_cases and executes get_levels_to_root for the current id, the max number found is returned.
A view object (read only) is created that executes this function (and is added to the application module):



A managed bean (request scope) is created to execute this view object and return the MaxNumber attribute as int:



The implementation of this bean:

package nl.capgemini.marianneHorsch.view;



import javax.faces.context.FacesContext;

import nl.capgemini.marianneHorsch.model.services.TestAppModuleImpl;

import nl.capgemini.marianneHorsch.model.views.MaxNumberOfChildrenViewImpl;

import nl.capgemini.marianneHorsch.model.views.MaxNumberOfChildrenViewRowImpl;



import oracle.adf.model.binding.DCBindingContainer;

import oracle.adf.model.binding.DCDataControl;

import oracle.binding.BindingContainer;



public class TestHierarchyViewBean {

public TestHierarchyViewBean() {

super();

}



public int getMaxNumberOfChildren() {

TestAppModuleImpl service = getService();

MaxNumberOfChildrenViewImpl view = service.getMaxNumberOfChildrenView();

view.executeQuery();

MaxNumberOfChildrenViewRowImpl row =

(MaxNumberOfChildrenViewRowImpl)view.first();

return (row == null ? 1 : (row.getMaxNumber() == null ? 1 :

row.getMaxNumber().intValue()));

}



public TestAppModuleImpl getService() {

DCBindingContainer bc = (DCBindingContainer)FacesContext

.getCurrentInstance().getApplication().evaluateExpressionGet

(FacesContext.getCurrentInstance(), "#{bindings}",

BindingContainer.class);

if (bc == null) {

return null;

}

DCDataControl dc = bc.findDataControl("TestAppModuleDataControl");

if (dc == null) {

return null;

}

return (TestAppModuleImpl)dc.getDataProvider();

}

}

Add to the <dvt:hierarchyViewer> tag the property displayLevelsChildren set the value to #{hierarchyViewerBean.maxNumberOfChildren}.

If we now run the page it opens like:



Function: get_levels_to_root



CREATE OR REPLACE FUNCTION get_levels_to_root

( i_n_cse_id IN tst_cases.id%TYPE)

RETURN NUMBER

AS

--

CURSOR c_cse (b_parent_id tst_cases.parent_cse_id%TYPE)

IS

SELECT id, name, parent_cse_id

FROM tst_cases

WHERE id = b_parent_id;

r_cse c_cse%ROWTYPE;

l_b_found BOOLEAN;

--

l_n_id tst_cases.id%TYPE;

l_n_levels NUMBER(10,0) := 0;

--

BEGIN

--

l_n_id := i_n_cse_id;

--

WHILE (l_n_id IS NOT NULL)

LOOP

--

OPEN c_cse (b_parent_id => l_n_id);

FETCH c_cse

INTO r_cse;

l_b_found := c_cse%FOUND;

CLOSE c_cse;

--

IF (l_b_found)

THEN

l_n_levels := l_n_levels + 1;

l_n_id := r_cse.parent_cse_id;

ELSE

l_n_id := NULL;

END IF;

--

END LOOP;

--

RETURN l_n_levels;

--

END get_levels_to_root;


Function: get_max_number_of_children



CREATE OR REPLACE FUNCTION get_max_number_of_children

RETURN NUMBER

AS

--

l_n_number NUMBER(10,0);

l_n_max_number NUMBER(10,0) := 0;

--

BEGIN
--

FOR r IN (SELECT id

FROM tst_cases)

LOOP

--

l_n_number := get_levels_to_root(i_n_cse_id =>r.id);

IF (l_n_number > l_n_max_number)

THEN

l_n_max_number := l_n_number;

END IF;

--

END LOOP;

--

RETURN l_n_max_number;

--

END get_max_number_of_children;

zondag 14 augustus 2011

How to restart a taskflow from the menu

Technology: ADF11g
Developed in: JDeveloper 11.1.1.4.0
Browsers tested: Firefox 3.6.13 and Internet explorer 7 (7.0.6002.18005)
Used database schema: HR
Used tables: EMPLOYEES, DEPARTMENTS


Summary



In the blog 'How to fix menu item navigation when using bounded taskflows' I explained how to abandon a taskflow so you could start another taskflow from the menu bar. This works fine as long as the taskflows are not started from a dynamic region.

When you use dynamic regions (so each bounded taskflow is created using page fragments) and the current application state is somewhere in a taskflow then I would expect if you use the menu bar and select the same taskflow as the application is currently in that it would restart the taskflow so it’s default start activity is executed. Without any interference this is not the case, when you use the menu and select the current taskflow nothing is happening. However if you select another taskflow there is no custom code needed to start a new taskflow.

In this blog a solution is provided to make the current taskflow restart when using the menu option.

Overview of the page flow, the red arrows indicate the current flow, the green arrow the requested behavior after clicking on the menu:



Setup example application



For this blog an example application is created based on the HR schema. The example application contains a table of employees with detail form and a departments table and detail form. The detail forms are created in a separate page. For the employees table and form a bounded task flow is created this bounded task flow is started from the menu. For the departments table and form another bounded task flow is created which is also started from the menu.

Model layer



Create the following entities:
Entity name Based on table of HR schema Customizations made
Employee EMPLOYEES None
Department DEPARTMENTS None


Create the following view objects:
View object name Based on entities Customizations made
EmployeesView Employee None
DepartmentsView Department None


Create an application module HrAppModule which exposes the EmployeesView and DepartmentsView.



Task flow




Unbounded task flow



The unbounded task flow only contains the declaration of the managed bean used for the dynamic region (scope = view):



Bounded task flow



The employees task flow and the departments task flow are bounded task flows. Both look like this (but for departments the view is DepartmentsTable and DepartmentDetails).



The next properties are set (for both bounded task flows):
Property Value
usePageFragments true (this is the default value)
Share data controls with calling task flow true




Home page



An empty page containing a default layout of 2 column and two rows (first fixed size second stretchable):



In the first row the menu is created:

<af:menuBar id="mb1">

<af:commandMenuItem id="cmi1"

text="Employees"

action="#{viewScope.dynamicRegionManager.startEmployeesTaskFlow}"

immediate="true"/>

<af:commandMenuItem id="cmi2"

text="Departments"

action="#{viewScope.dynamicRegionManager.startDepartmentsTaskFlow}"

immediate="true"/>

</af:menuBar>

In the stretchable panel the dynamic region is defined:

<af:panelGroupLayout layout="scroll"

xmlns:af="http://xmlns.oracle.com/adf/faces/rich"

id="pgl1">

<af:region id="r1"

value="#{bindings.dynamicRegion1.regionModel}"

partialTriggers="::cmi1 ::cmi2"/>

</af:panelGroupLayout>

This can be done by dragging and dropping a bounded taskflow in the center facet then the page definition is created and filled automatically and the managed bean can be selected, after dragging and dropping the partial triggers should be added manually to the region.

DynamicRegionManager



This managed bean class contains methods for each menu item and a method to get the current taskflow:

public class DynamicRegionManager {

private String taskFlowId = "/WEB-INF/EmployeesFlow.xml#EmployeesFlow";



public DynamicRegionManager() {

}



public TaskFlowId getDynamicTaskFlowId() {

return TaskFlowId.parse(taskFlowId);

}



public String startEmployeesTaskFlow() {

taskFlowId = "/WEB-INF/EmployeesFlow.xml#EmployeesFlow";

return null;

}

public String startDepartmentsTaskFlow() {

taskFlowId = "/WEB-INF/DepartmentsFlow.xml#DepartmentsFlow";

return null;

}

}


Table page



The table pages are created by drag and drop from the Data Controls. The table is dropped as ADF Read-only table with Single Row Selection checked, all columns are displayed.

For display purposes the styleclass is set:
PropertyValue
styleClassAFStretchWidth


Underneath the table two buttons are added to go to the details page and to end the current task flow:

<af:panelGroupLayout id="pg1"

layout="horizontal">

<af:commandButton id="cb1"

text="Show details"

action="detail"/>

<af:commandButton id="cb2"

text="End"

action="end"/>

</af:panelGroupLayout>


Form page



The form pages are created by drag and drop from the Data Controls. The form is dropped as ADF Form all attributes are displayed and a submit button is added.

Underneath the form in the footer two more buttons are added to navigate back to the table and to end the taskflow. A rollback operation is added (form the data control palette) on top of the back button:

<af:group id="g1">

<af:commandButton id="cb1"

text="Submit"

actionListener="#{bindings.Commit.execute}"/>

<af:commandButton id="cb3"

text="Back"

actionListener="#{bindings.Rollback.execute}"

immediate="true"

action="back">

<af:resetActionListener/>

</af:commandButton>

<af:commandButton id="cb2"

text="End"

action="end"/>

</af:group>


Fix the menu navigation



When the application described above is deployed the navigation from the menu does not work (properly) if you’re in the details page and then click on the menu entry of the same taskflow.

The reason the menu doesn’t work is that the region concludes you’re already in this taskflow so no actions are needed.

A solution to this issue is to force the refresh of the region. This can be done by binding the region to the dynamicRegionManager bean instance and whenever a menu item is clicked (the action of the dynamicRegionManager will be executed) force a refresh:
  1. Add the binding to the region tag:

    <af:region id="r1"

    value="#{bindings.dynamicRegion1.regionModel}"

    partialTriggers="::cmi1 ::cmi2"

    binding="#{viewScope.dynamicRegionManager.dynamicRegion}"/>

  2. Define the region in the DynamicRegionManager class (include accessors):

    private RichRegion dynamicRegion;

  3. Refresh the region when an action is triggered:

    public String startEmployeesTaskFlow() {

    taskFlowId = "/WEB-INF/EmployeesFlow.xml#EmployeesFlow";

    getDynamicRegion().refresh(FacesContext.getCurrentInstance());

    return null;

    }

    public String startDepartmentsTaskFlow() {

    taskFlowId = "/WEB-INF/DepartmentsFlow.xml#DepartmentsFlow";

    getDynamicRegion().refresh(FacesContext.getCurrentInstance());

    return null;

    }

Now the menu items respond like we requested, if logging is added to the initializer and finalizer methods of the bounded taskflows you can see that on refresh the finalizer of the taskflow is executed followed by the initializer. If custom actions are needed (for example of you navigate from the table to the details with a new button that does an insert of a new row it could be usefull to do a remove of this row in the finalizer so that the created new row is removed from the view object, otherwise there is a risk on a commit in a different page that this new line will be inserted in the database).

vrijdag 4 maart 2011

How to create a custom LOV

Technology: ADF11g
Developed in: JDeveloper 11.1.1.3.0
Browsers tested: Firefox 3.6.13 and Internet explorer 7 (7.0.6002.18005)
Used database schema: HR
Used tables: EMPLOYEES, DEPARTMENTS, LOCATIONS


Summary

In this blog we create ‘standard’ LOVs and a custom LOV. A custom LOV is in the end created because of some disadvantages I see in the standard LOVs like:
  • Possibility to resize LOV popup
  • All columns in the LOV table have the same width

Standard LOV

First we’ll create a standard LOV. With a standard LOV I mean a LOV defined in the view object as a List of Values on the ID column.

For this example the EMPLOYEES table is used with LOVs on the ManagerId attribute and DepartmentId attribute (the DepartmentId LOV is created in the next paragraph).

Model layer

Create the following entities:

Entity name Based on table of HR schema Customizations made
Employee EMPLOYEES None


Create the following view objects:

View object name Based on entities Customizations made
EmployeesLovView Employee None
ManagersView Employee None


Open the EmployeesLovView and go to the ManagerId attribute. Add a List of Values:



The ManagersView is the view accessor (renamed to ManagersAccessor), the view attribute is the ManagerId and the list attribute the EmployeeId. On the UI Hints tab set the following properties:

Property Value
Default List Type Input Text with List of Values
Display Attributes Select all
List Search <No Search>
"Include No Selection" Item Deselect all


Create an application module HrAppModule which exposes the EmployeesView.

Page

A form page is created by drag and drop from the Data Controls. The form is dropped as ADF Form only the EmployeeId, FirstName, LastName, JobId and ManagerId items are displayed. All items except ManagerId are displayed as Output text with label. ManagerId is displayed as input list of values:





Select Include Navigation Controls and Include Submit Button.

If we run this page it looks like this:



The opened popup contains scrollbars and is not resizable. When a row is selected (by double click or by using the OK button) the ID of the employee is copied to the ManagerId attribute.

Extended standard LOV

The first thing I thing needs to be changed is that the ManagerId is displayed. I think the managers name is a more desirable field to see (of course in the database the manager ID should be filled).

To create an example to show how this can be achieved another LOV is created for the department. In the screen we’ll display the department name and the LOV is on this name but the department ID is filled as well (to prove this the department ID will be made visible but read only in the page).

Model layer

Create the following entities:

Entity name Based on table of HR schema Customizations made
Department DEPARTMENTS None
Location LOCATIONS None


A FK association will be created at the same time between Department and Location.

Create the following view objects:

View object name Based on entities Customizations made
DepartmentsLocationView Department

Location
None


Change the EmployeesLovView:
  • Add a transient attribute DepartmentLovName of type String
  • Make the attribute always updatable
  • Check the Mapped to Column or SQL checkbox
  • Fill the expression of the Query Column:
    • (SELECT department_name FROM departments WHERE department_id = Employee.department_id)
  • Set the type of the Query Column to VARCHAR2(30)


The last 3 steps are necessary to display the current department name when the page is opened.
  • Add a List of Values to the DepartmentLovName attribute:


The DepartmentsLocationView is the view accessor (renamed to DepartmentsLocationAccessor), the view attribute is the DepartmenLovName and the list attribute the DepartmentName.

Add a second return value from DepartmentId to DepartmentId:



On the UI Hints tab set the following properties:

Property Value
Default List Type Input Text with List of Values
Display Attributes Select all
List Search <No Search>
"Include No Selection" Item Deselect all



Page

Drop the DepartmentId and DepartmentLovName attribute from the Data Controll palette onto the page created for the Manager LOV. The DepartmentId is displayed as Output text with label, the DepartmentLovName as Input list of values.

If we run this page it looks like this:



When a row is selected (by double click or by using the OK button) the Name and the ID of the department is copied to the page. That is an improvement compared to the Manager ID. But the opened popup still contains scrollbars and is not resizable.

Create a custom (resizable) LOV

Finally we’ll create a resizable LOV. This is also done for the department.

Model layer

Change the EmployeesLovView (make a copy of DepartmentLovName):
  • Add a transient attribute DepartmentLovName2 of type String
  • Make the attribute always updatable
  • Check the Mapped to Column or SQL checkbox
  • Fill the expression of the Query Column:
    • (SELECT department_name FROM departments WHERE department_id = Employee.department_id)
  • Set the type of the Query Column to VARCHAR2(30)

Page

Drop the DepartmentId (same as used for the department LOV) and DepartmentLovName2 attribute from the Data Controll palette onto the page underneath a seperator. The DepartmentId is displayed as Output text with label, the DepartmentLovName2 as Input text with label.

Surround the DepartmentLovName2 input text in a panelGroup with layout style horizontal. After the input text add a command image link with the LOV icon. The command link triggers a popup:

<af:panelGroupLayout id="lovGroup"

layout="horizontal">

<af:inputText value="#{bindings.DepartmentLovName21.inputValue}"

label="#{bindings.DepartmentLovName21.hints.label}"

required="#{bindings.DepartmentLovName21.hints.mandatory}"

columns="#{bindings.DepartmentLovName21.hints.displayWidth}"

maximumLength="#{bindings.DepartmentLovName21.hints.precision}"

shortDesc="#{bindings.DepartmentLovName21.hints.tooltip}"

id="departmentLovNameInputText">

<f:validator binding="#{bindings.DepartmentLovName21.validator}"/>

</af:inputText>

<af:commandImageLink id="departmentLovNameCommandLink"

icon="/common/images/lov.png">

<af:showPopupBehavior popupId="departmentLovPopup"

triggerType="action"/>

</af:commandImageLink>

</af:panelGroupLayout>

The picture (lov_ena.png) can be copied from:
  • Run your application, in the IntegratedWebLogicServer log you see a destination where the EAR is copied to. On my computer is that:
    • [03:25:38 PM] Wrote Web Application Module to C:\Users\mhorsch\AppData\Roaming\JDeveloper\system11.1.1.3.37.56.60\o.j2ee\drs\HR_demo_app\ViewControllerWebApp.war
  • Go to the o.j2ee folder and in that folder go to: \.wlLibs\jsp
  • Open the ADF-Faces-Components11.war
  • Go to \WEB-INF\lib and open the adf-richclient-impl-11.jar jar file
  • Go to adf\images
The popup only contains the DepartmentsLocationView as read only table. First create the popup:

<af:popup id="departmentLovPopup">

<af:dialog id="lovDialog"

title="LOV"

type="okCancel"

dialogListener="#{employeeBean.lovDialogListeners}"

resize="on"

contentWidth="600"

stretchChildren="first">

</af:dialog>

</af:popup>

This popup is 600 pixels width by default and resizable. If the dialog is closed lovDialogListeners of the employeeBean is called.

This bean class is defined in the unbound task flow:

Managed bean property Value
Name employeeBean
Class nl.hr.demo.view.beans.EmployeeBean
Scope request


The DepartmentsLocationView table is inserted in the dialog by drag and drop from the data control palette:





The Row Selection property must be checked, this causes the following properties to be set in the table:

Property Value
selectedRowKeys #{bindings. DepartmentsLocationView.collectionModel.selectedRow}
selectionListener #{bindings. DepartmentsLocationView.collectionModel.makeCurrent}
rowSelection single


Although in the JSPX page the selectedRowKeys and selectionListener statements contains warnings that the references methods cannot be found they can be found runtime.

In the EmployeeBean we have to implement the lovDialogListeners method. The method is only executed when the LOV is closed by the OK button. In the method the department ID and name of the selected row are copied to the EmployeesLovView row:

package nl.hr.demo.view.beans;



import javax.faces.context.FacesContext;

import nl.hr.demo.model.services.HrAppModuleImpl;

import oracle.adf.model.binding.DCBindingContainer;

import oracle.adf.model.binding.DCDataControl;

import oracle.adf.view.rich.event.DialogEvent;

import oracle.binding.BindingContainer;

import oracle.jbo.domain.Number;

import oracle.jbo.server.ViewRowImpl;



public class EmployeeBean {

public EmployeeBean() {

super();

}



private static HrAppModuleImpl getService() {

DCBindingContainer bc = (DCBindingContainer)FacesContext.getCurrentInstance().getApplication().evaluateExpressionGet(FacesContext.getCurrentInstance(), "#{bindings}", BindingContainer.class);

DCDataControl dc = bc.findDataControl("HrAppModuleDataControl");

return (HrAppModuleImpl) dc.getDataProvider();

}



public void lovDialogListeners(DialogEvent dialogEvent) {

ViewRowImpl row = (ViewRowImpl)getService().getDepartmentsLocationView().getCurrentRow();

if (row != null) {

Number departmentId = (Number) row.getAttribute("DepartmentId");

String departmentName = (String)row.getAttribute("DepartmentName");

ViewRowImpl employeeRow = (ViewRowImpl)getService().getEmployeesLovView().getCurrentRow();

employeeRow.setAttribute("DepartmentId", departmentId);

employeeRow.setAttribute("DepartmentLovName2", departmentName);

}

}

}

If we run this page it looks like this:



The popup is resizable, the name and id are copied and refreshed and the width of each column can be modified individually (by setting the width property of the af:column).

donderdag 3 maart 2011

How to convert input to uppercase

Technology: ADF11g
Developed in: JDeveloper 11.1.1.3.0
Browsers tested: Firefox 3.6.13 and Internet explorer 7 (7.0.6002.18005)
Used database schema: HR
Used tables: EMPLOYEES


Summary

In this blog four different solutions are provided to convert text to uppercase:
  • Convert to uppercase in the model layer
  • Convert to uppercase with java script
  • Convert to uppercase in a java bean
  • Convert to uppercase using a converter
The first and last two solutions only change the complete text to uppercase when the user leaves the input field. The second solution immediately changes each letter the user types to uppercase.

The third and fourth solution is created for the scenario that the input field is not bound to the model layer. But they can also be used if they are bound to the model.

Setup example application

For this blog the EMPLOYEES table of the HR schema is used. An input form containing a few input fields of this table is created.

Model layer

Create the following entities:

Entity name Based on table of HR schema Customizations made
Employee EMPLOYEES None


Create the following view objects:

View object name Based on entities Customizations made
EmployeesView Employee None


Create an application module HrAppModule which exposes the EmployeesView.


Form page

The form page is created by drag and drop from the Data Controls. The form is dropped as ADF Form only the FirstName, LastName, Email, PhoneNumber and JobId items are displayed (the text items).



Employee bean

In the form page we will make references to a java bean class. This bean class is defined in the unbound task flow:

Managed bean property Value
Name employeeBean
Class nl.hr.demo.view.beans.EmployeeBean
Scope Request

Solution 1: Convert to uppercase in the model layer

This solution will be applied to the first and last name attributes. To create a generic solution a new java class is created in a Common project. The Model and ViewController projects contain dependencies to this Common project.

The java class created is called HrEntityObjectImpl and it extends EntityImpl. The Employee entity object extends HrEntityObjectImpl.

HrEntityObjectImpl

The implementation of this class:

package nl.hr.demo.model.common;



import oracle.jbo.AttributeDef;

import oracle.jbo.server.EntityImpl;



public class HrEntityObjectImpl extends EntityImpl {

public HrEntityObjectImpl() {

super();

}



@Override

protected void setAttributeInternal(int i, Object object) {

AttributeDef attrDef = getEntityDef().getAttributeDef(i);

if (attrDef != null && attrDef.getJavaType().getName().equals("java.lang.String")) {

String caseProperty = getEntityDef().getAttributeDef(i).getProperty("CASE").toString();

if (caseProperty != null && !caseProperty.equals("")) {

String value = (String)object;

if (caseProperty.equals("UPPER")) {

value = value.toUpperCase();

} else if (caseProperty.equals("LOWER")) {

value = value.toLowerCase();

}

object = value;

}

}

super.setAttributeInternal(i, object);

}

}

The overridden method looks for each attribute if it contains a CASE property. If it contains this property and the value is UPPER then the value is set to uppercase, if it is LOWER than the value is set to lower case.

Employee entity

Let the Employee entity extend the created HrEntityObjectImpl class. This can be done in the Java tab, click edit and then Classes Extend:



For the FirstName and LastName attributes create a custom property CASE with value UPPER, this can be done in the Attribute tab, double click on the attribute and add a Custom Property:



Form page

In the form page adjust the FirstName and LastName attributes, set the autoSubmit and partialTrigger property. When the user leaves the field the attribute will be submit to the model layer and refreshed.

Set the autoSubmit property to true and the partialTrigger to its own id. Example of LastName:

<af:inputText value="#{bindings.LastName.inputValue}"

label="#{bindings.LastName.hints.label}"

required="#{bindings.LastName.hints.mandatory}"

columns="#{bindings.LastName.hints.displayWidth}"

maximumLength="#{bindings.LastName.hints.precision}"

shortDesc="#{bindings.LastName.hints.tooltip}"

id="it6"

autoSubmit="true"

partialTriggers="it6">

<f:validator binding="#{bindings.LastName.validator}"/>

</af:inputText>

The result when the user is typing:


And after leaving the field:


Solution 2: Convert to uppercase with java script

This solution will be applied to the email attribute. To create a generic solution a java script file is created instead of defining the java code in the page.

Because using the af:resource tag in a page template resulted in not finding the java script method the Trinidad tag is used instead to include the java script:

<trh:script source="../../common/javaScript/hrScript.js" id="hrScript"></trh:script>

Note: to include the Trinidad tag library open the project properties of the ViewController, go to the JSP Tag Library tab and includeTrinidad Components 1.2 (prefix tr) and Trinidad HTML Components 1.2 (prefix trh) in the jsp root of the page (template) add:

xmlns:trh=http://myfaces.apache.org/trinidad/html

The java script method will be executed for each keyboard button. Convert the input the uppercase is only necessary when the button is a letter. The java script method:

function isLetter(keyCode) {

if ( keyCode == "A" || keyCode == "B" || keyCode == "C" || keyCode == "D"

|| keyCode == "E" || keyCode == "F" || keyCode == "G" || keyCode == "H"

|| keyCode == "I" || keyCode == "J" || keyCode == "K" || keyCode == "L"

|| keyCode == "M" || keyCode == "N" || keyCode == "O" || keyCode == "P"

|| keyCode == "Q" || keyCode == "R" || keyCode == "S" || keyCode == "T"

|| keyCode == "U" || keyCode == "V" || keyCode == "W" || keyCode == "X"

|| keyCode == "Y" || keyCode == "Z") {

return true;

}

return false;

}



function convertCase(event) {

var keyChar = AdfKeyStroke.getKeyStroke(event.getKeyCode()).toMarshalledString();

if (isLetter(keyChar)) {

var field = event.getCurrentTarget();

var value = field.getSubmittedValue();

field.setValue(value.toUpperCase());

return true;

}

}

In the page we have to trigger this java script method. This is done by adding a clientListener to the email attribute that triggers the java script everytime the user releases a key:

<af:inputText value="#{bindings.Email.inputValue}"

label="#{bindings.Email.hints.label}"

required="#{bindings.Email.hints.mandatory}"

columns="#{bindings.Email.hints.displayWidth}"

maximumLength="#{bindings.Email.hints.precision}"

shortDesc="#{bindings.Email.hints.tooltip}"

id="it1">

<af:clientListener type="keyUp"

method="convertCase"/>

</af:inputText>

Now it’s impossible to make a screen shot of lower case letters:


Solution 3: Convert to uppercase in a java bean

For this solution the model is not used. In new inputText is added to the page instead:

<af:inputText value="#{employeeBean.transientField}"

autoSubmit="true"

partialTriggers="transientField"

label="Transient field"

id="transientField"/>

This item is bound to the employeeBean. A class variable transientField (String) is added and its accessors.

The result when the user is typing:


And after leaving the field:


Solution 4: Convert to uppercase using a converter

This solution will be applied to the job ID attribute.

A custom converter class is created which implements the Converter interface. This class converts given string to uppercase:

package nl.hr.demo.view.converters;



import javax.faces.component.UIComponent;

import javax.faces.context.FacesContext;

import javax.faces.convert.Converter;



public class UpperCaseConverter implements Converter {

public UpperCaseConverter() {

super();

}



public String getAsString(FacesContext fc, UIComponent uIComponent, Object object) {

return object.toString();

}

public Object getAsObject(FacesContext fc, UIComponent uIComponent, String string) {

return string.toUpperCase();

}

}

The converter is added to the faces-config.xml so it can be used in the pages:



In the form page adjust the JobId attribute, set the converter, autoSubmit and partialTrigger property. When the user leaves the field the attribute will be submit to the model layer and refreshed.

Set the converter to the created converter (id), autoSubmit property to true and the partialTrigger to its own id:

<af:inputText value="#{bindings.JobId.inputValue}"

label="#{bindings.JobId.hints.label}"

required="#{bindings.JobId.hints.mandatory}"

columns="#{bindings.JobId.hints.displayWidth}"

maximumLength="#{bindings.JobId.hints.precision}"

shortDesc="#{bindings.JobId.hints.tooltip}"

id="it2"

partialTriggers="it2"

autoSubmit="true"

converter="upperCaseConverter">


</af:inputText>

The result when the user is typing:


And after leaving the field:

woensdag 2 maart 2011

How to create web service based ADF pages

Technology: ADF11g
Developed in: JDeveloper 11.1.1.3.0
Used database schema: none


Summary

In this blog a solution is provided how to create a web service client. This blog is part of a sequence of 3 blogs:
  • How to create a RESTful web service that returns objects
  • How to create a web service client
  • How to create web service based ADF pages
I choose for a solution that:
  • Works for SOAP and RESTful web services
  • The location of the web service host can be changed while running the web service
Especially changing the host was an important criteria for me. For development I would like to use a development webservice and for production of course a production server.

The solution is build into my existing demo ADF application.

Step 1

Create a Common (generic) project in the demo ADF application add the following libraries:
  • BC4J Oracle Domains
  • Oracle JDBC
  • JSF 1.2
  • Commons Logging 1.0.4
And add the HrWebServiceClient.jar created in the ‘How to create a web service client’ blog.

Open the project properties of the Model project and set the dependency to the Common project:



Open the project properties of the ViewController project and also set the dependency to the Common project.

Step 2

In the Common project a basic view object implementation is created that will be used for all web service based view objects.

The class contains converters that converts Oracle data types to Java data types, it sets the end point based on the information of the web.xml (in the ViewController project) and has methods to execute the webservice.

Create a abstract java class WsViewObjectImpl which extends ViewObjectImpl. The implementation:

package nl.hr.demo.model.common;



import java.sql.ResultSet;

import java.sql.SQLException;

import java.sql.Timestamp;

import java.util.Iterator;

import java.util.List;

import javax.faces.context.FacesContext;

import oracle.jbo.server.ViewObjectImpl;

import oracle.jbo.server.ViewRowImpl;

import oracle.jbo.server.ViewRowSetImpl;



public abstract class WsViewObjectImpl extends ViewObjectImpl {

public WsViewObjectImpl() {

super();

}



protected String getWSBaseURL() {

return FacesContext.getCurrentInstance().getExternalContext().getInitParameter("wsBaseURL");

}



protected oracle.jbo.domain.Date toOracleDate(java.util.Calendar calendar) {

oracle.jbo.domain.Date jboDate = new oracle.jbo.domain.Date();

Timestamp t = jboDate.timestampValue();

t.setTime(calendar.getTime().getTime());

return new oracle.jbo.domain.Date(t);

}

protected oracle.jbo.domain.Number toOracleNumber(int i) {

return new oracle.jbo.domain.Number(i);

}

protected oracle.jbo.domain.Number toOracleNumber(double d) {

try {

return new oracle.jbo.domain.Number(d);

} catch (SQLException e) {

return null;

}

}



protected abstract List retrieveArrayFromWebService(Object qc, Object[] params);

private void storeNewIterator(Object qc, List rs) {

setUserDataForCollection(qc, rs.iterator());

hasNextForCollection(qc);

}

protected void executeQueryForCollection(Object qc, Object[] params, int noUserParams) {

storeNewIterator(qc, retrieveArrayFromWebService(qc, params));

super.executeQueryForCollection(qc, params, noUserParams);

}



private Iterator getArrayIterator(Object qc) {

return (Iterator)getUserDataForCollection(qc);

}

protected boolean hasNextForCollection(Object qc) {

boolean hasNext = getArrayIterator(qc).hasNext();

if (!hasNext) {

setFetchCompleteForCollection(qc, true);

}

return hasNext;

}



protected abstract void fillRow(Object data, ViewRowImpl row);

protected ViewRowImpl createRowFromResultSet(Object qc, ResultSet resultSet) {

Iterator iterator = getArrayIterator(qc);

ViewRowImpl row = createNewRowForCollection(qc);

fillRow(iterator.next(), row);

return row;

}



public long getQueryHitCount(ViewRowSetImpl viewRowSet) {

return super.getQueryHitCount(viewRowSet);

}

}


Step 3

Create a new programmatic view object in the Model project. The new view object:
  • Is programmatically
  • Extends the WsViewObjectImpl class
  • Contains a RowImpl class
  • Has a bind variable b_id data type Number
  • All attributes (of the EmployeeClient) are updatable
  • The EmployeeId attribute is Key Attribute.




Open the view object impl class (EmployeesWsImpl) and implement it (only added / changed methods are described below):

package nl.hr.demo.model.views.ws;



import java.util.ArrayList;

import java.util.List;

import nl.hr.demo.webservices.Employee;

import nl.hr.demo.webservices.client.EmployeeClient;

import oracle.jbo.server.ViewRowImpl;



public class EmployeesWsImpl extends WsViewObjectImpl {

protected List retrieveArrayFromWebService(Object qc, Object[] params) {

try {

EmployeeClient client = new EmployeeClient(getWSBaseURL());

if (getb_id() == null) {

return client.getAllEmployees();

} else {

List<Employee> result = new ArrayList<Employee>();

result.add(client.getEmployeeById(getb_id().intValue()));

return result;

}

} catch (Exception e) {

e.printStackTrace();

return null;

}

}



protected void fillRow(Object data, ViewRowImpl row) {

Employee empe = (Employee) data;

populateAttributeForRow(row, EmployeesWsRowImpl.EMPLOYEEID, toOracleNumber(emp.getEmployeeId()));

populateAttributeForRow(row, EmployeesWsRowImpl.FIRSTNAME, emp.getFirstName());

populateAttributeForRow(row, EmployeesWsRowImpl.LASTNAME, emp.getLastName());

populateAttributeForRow(row, EmployeesWsRowImpl.HIREDATE, toOracleDate(emp.getHireDate()));

populateAttributeForRow(row, EmployeesWsRowImpl.EMAIL, emp.getEmail());

populateAttributeForRow(row, EmployeesWsRowImpl.PHONENUMBER, emp.getPhoneNumber());

populateAttributeForRow(row, EmployeesWsRowImpl.JOBID, emp.getJobId());

populateAttributeForRow(row, EmployeesWsRowImpl.SALARY, toOracleNumber(emp.getSalary()));

populateAttributeForRow(row, EmployeesWsRowImpl.COMMISSIONPCT, toOracleNumber(emp.getCommissionPct()));

populateAttributeForRow(row, EmployeesWsRowImpl.MANAGERID, toOracleNumber(emp.getManagerId()));

populateAttributeForRow(row, EmployeesWsRowImpl.DEPARTMENTID, toOracleNumber(emp.getDepartmentId()));

}

}


Step 4

Add the view object to the application module.

Step 5

Create a new page: EmployeesWebServiceTable

Drag and drop the EmployeesWs onto it as read only table:



Add above the table a panelFormLayout containing:

<af:panelFormLayout id="pfl1">

<af:inputText binding="#{employeeBean.id}"

label="ID"

id="idFilter">

<af:convertNumber groupingUsed="false"

integerOnly="true"/>

</af:inputText>

<f:facet name="footer">

<af:commandButton text="Filter"

id="filter"

actionListener="#{employeeBean.filterWs}"/>

</f:facet>

</af:panelFormLayout>

Add to the af:table tag:

partialTriggers="::filter"

styleClass="AFStretchWidth"


Step 6

Create a java class EmployeeBean for the page. This bean class holds the ID input text and queries the view object when the filter button is used.

package nl.hr.demo.view.beans;



import javax.faces.context.FacesContext;

import nl.hr.demo.model.services.HrAppModuleImpl;

import nl.hr.demo.model.views.ws.EmployeesWsImpl;

import oracle.adf.model.binding.DCBindingContainer;

import oracle.adf.model.binding.DCDataControl;

import oracle.adf.view.rich.component.rich.input.RichInputText;

import oracle.binding.BindingContainer;

import oracle.jbo.domain.Number;



public class EmployeeBean {



private RichInputText id;



public EmployeeBean() {

super();

}



public void filterWs (ActionEvent actionEvent) {

EmployeesWsImpl view = getService().getEmployeesWs();

view.setb_id(getValue(id));

view.executeQuery();

}

private Number getValue(RichInputText item) {

if (item == null || item.getValue() == null) {

return null;

}

return new Number(((Long)item.getValue()).intValue());

}



private static HrAppModuleImpl getService() {

DCBindingContainer bc = (DCBindingContainer)FacesContext.getCurrentInstance().getApplication().evaluateExpressionGet(FacesContext.getCurrentInstance(), "#{bindings}", BindingContainer.class);

DCDataControl dc = bc.findDataControl("HrAppModuleDataControl");

return (HrAppModuleImpl) dc.getDataProvider();

}



public void setId(RichInputText id) {

this.id = id;

}

public RichInputText getId() {

return id;

}

}


Step 7

Add the page and the bean to the unbounded task flow:



Managed bean property Value
Name employeeBean
Class nl.hr.demo.view.beans.EmployeeBean
Scope Request

Step 8

Start the page it opens getAllEmployees is executed of the web service:



And if you fill an ID and click filter the web service is called again: