woensdag 2 maart 2011

How to create a web service client

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.

Step 1

Create a new generic application in JDeveloper, call the project RpcWebServiceProject:



Step 2

On the project choose New – Web Service Proxy (under Business Tier – Web Services).


Choose:
  • Client Style: JAX-RPC Web Logic Style
  • Copy the URL or the location of the WSDL file
    • For this example we use: http://localhost:7101/HrWebServices-HrWebServicesProject-context-root/EmployeesWebPort?WSDL
  • Specify a package name:
    • For example: nl.hr.demo.webservice.client.employees
  • Leave all other settings to their default
A lot of files have been created in the project. The EmployeesWebPortClient class contains the entry point of calling the web service.

To make maintenance easier create a new project where a new entry point for each web service is created. If the web service ever changes then the sources in the RpcWebServiceProject project can be regenerated (right click on the EmployeesWebServiceProxy – Regenerate web service) and know custom code get lost.

Step 3

Create a new project HrWebServiceProject containing two java classes:
  • EmployeeClient (package nl.hr.demo.webservices.client)
  • HrWebServiceClient (package nl.hr.demo.webservices)
Set the dependencies of the HrWebServiceProject, add RpcWebServiceProject.

The HrWebServiceClient class is a generic class that creates the WSDL URL for given server location and contains methods to convert data types:

package nl.hr.demo.webservices;



import java.math.BigInteger;

import java.net.MalformedURLException;

import java.net.URL;

import java.util.ArrayList;

import java.util.List;



public class HrWebServiceClient {

private String webserviceBaseURL;



public HrWebServiceClient() {

super();

}

public HrWebServiceClient(String webserviceBaseURL) {

this.webserviceBaseURL = webserviceBaseURL;

}



protected String getWebserviceUrlAsString(String webservice) {

return webserviceBaseURL + "/" + webservice;

}

protected URL getWebserviceUrl(String webservice) {

URL wsdlLocationURL = null;

try {

wsdlLocationURL = new URL(getWebserviceUrlAsString(webservice));

} catch (MalformedURLException e) {

return null;

}

return wsdlLocationURL;

}



protected List convertToList(Object[] input) {

List list = new ArrayList();

for (int i = 0; i < input.length; i++) {

list.add(input[i]);

}

return list;

}

}

The EmployeeClient extends the HrWebServiceClient:

package nl.hr.demo.webservices.client;



import java.util.List;



import nl.hr.demo.webserivce.client.employees.EmployeesWebPortClient;

import nl.hr.demo.webservices.HrWebServiceClient;

import nl.hr.demo.webservices.Employee;



public class EmployeeClient extends HrWebServiceClient {

private static final String WSDL_NAME = "HrWebServices-HrWebServicesProject-context-root/EmployeesWebPort?WSDL";



public EmployeeClient() {

super();

}

public EmployeeClient(String webserviceBaseURL) {

super(webserviceBaseURL);

}



private EmployeesWebPortClient getClient() throws Exception {

EmployeesWebPortClient client = new EmployeesWebPortClient();

client.setPortCredentialProviderList();

client.setEndpoint(getWebserviceUrlAsString(WSDL_NAME));

return client;

}



public Employee getEmployeeById(int id) throws Exception {

EmployeesWebPortClient client = getClient();

return client.getEmployeeById(id);

}



public List getAllEmployees() throws Exception {

EmployeesWebPortClient client = getClient();

Employee[] result = client.getAllEmployees(null).getReturn();

return convertToList(result);

}

}

For each method in the web service a method is created in the java class. For test purposes some code can be added so the web service client can be executed from JDeveloper:

public static void main(String[] args) {

String LOCALHOST = "http://localhost:7101";

try {

System.err.println("Search employee ID 100");

Employee emp = new EmployeeClient(LOCALHOST).getEmployeeById(100);

print(emp);

System.err.println("Search employee ID 101");

emp = new EmployeeClient(LOCALHOST).getEmployeeById(101);

print(emp);

System.err.println("Search employee ID 102");

emp = new EmployeeClient(LOCALHOST).getEmployeeById(102);

print(emp);

System.err.println("Search all employees");

List result = new EmployeeClient(LOCALHOST).getAllEmployees();

for (int i = 0; i < result.size(); i++) {

print(result.get(i));

}

if (result.size() == 0) {

System.err.println("niets terug");

}

} catch (Exception e) {

e.printStackTrace();

}

}



private static void print(Employee employee) {

System.err.println(employee.getEmployeeId() + ": " +

employee.getFirstName() + " " +

employee.getLastName());

}

The result of this is:

Search employee ID 100

100: Steven King

Search employee ID 101

101: Neena Kochhar

Search employee ID 102

102: Lex De Haan

Search all employees

100: Steven King

101: Neena Kochhar

102: Lex De Haan


Step 4

Create a deployment profile (JAR file) for the HrWebServiceProject and create the jar file. This file will be needed in the next blog ‘How to create web service based ADF pages’.

dinsdag 1 maart 2011

How to create a RESTful web service that returns objects

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 RESTful web service that returns objects. One web service will be created with 2 methods to return an Employee instance and a list of Employee instances.

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

Step 1

Create a new generic application in JDeveloper:



Step 2

Download the 'zip of Jersey' of the page:

http://jersey.java.net/nonav/documentation/latest/chapter_deps.html

Step 3

Extract the zip and copy the following jar files to the new created application (in a lib folder):
  • Asm-3.1.jar
  • Jersey-core-1.5.jar
  • Jersey-server-1.5.jar
  • Jersey-json-1.5.jar

Step 4

Open the project properties of the project in the new application and add these jar files (not the JAX-WS Web Services library will be added automatically later on):



Step 5

Create an Employee class which represents one employee (of the HR schema). Create accessors for all class variables. A second constructor is added so an employee can be created and initialized in one call.

Add above the class name the annotation XmlRootElement of class javax.xml.bind.annotation.XmlRootElement.

package nl.hr.demo.dataObjects;



import java.util.Date;

import javax.xml.bind.annotation.XmlRootElement;



@XmlRootElement

public class Employee {

private int employeeId;

private String firstName;

private String lastName;

private String email;

private String phoneNumber;

private Date hireDate;

private String jobId;

private double salary;

private double commissionPct;

private int managerId;

private int departmentId;



public Employee() {

super();

}

public Employee(int employeeId, String firstName, String lastName, String email, String phoneNumber, Date hireDate, String jobId, double salary, double commissionPct, int managerId, int departmentId) {

super();

setEmployeeId(employeeId);

setFirstName(firstName);

setLastName(lastName);

setEmail(email);

setPhoneNumber(phoneNumber);

setHireDate(hireDate);

setJobId(jobId);

setSalary(salary);

setCommissionPct(commissionPct);

setManagerId(managerId);

setDepartmentId(departmentId);

}



// For all class variables create accessors

public void setXXX (XXX xxx) {

this.xxx = xxx;

}

public XXX getXXX () {

return xxx;

}

}


Step 6

Create the web service class that returns one or a list of employees. If a web service contains several (GET) methods each method should have its own path.

package nl.hr.demo.webservices;



import java.text.SimpleDateFormat;

import java.util.ArrayList;

import java.util.List;

import javax.ws.rs.GET;

import javax.ws.rs.Path;

import javax.ws.rs.Produces;

import javax.ws.rs.QueryParam;

import nl.hr.demo.dataObjects.Employee;



@Path("EmployeesService")

public class EmployeesServices {

public EmployeesServices() {

super();

}



@GET

@Path("getEmployeeById")

@Produces("application/json")

public Employee getEmployeeById(@QueryParam("id")int id) {

SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy");

try {

if (id == 100) {

return new Employee(id, "Steven", "King", "SKING", "515.123.4567", sdf.parse("17-06-1987"), "AD_PRES", 24000, -1, -1, 90);

} else if (id == 101) {

return new Employee(id, "Neena", "Kochhar", "NKOCHHAR", "515.123.4568", sdf.parse("21-09-1989"), "AD_VP", 17000, -1, 100, 90);

}

return new Employee(id, "Lex", "De Haan", "LDEHAAN", "515.123.4569", sdf.parse("13-01-1993"), "AD_VP", 17000, -1, 100, 90);

} catch (Exception e) {

return null;

}

}



@GET

@Path("getAllEmployees")

@Produces("application/json")

public List<Employee> getAllEmployees() {

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

SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy");

try {

result.add(new Employee(100, "Steven", "King", "SKING", "515.123.4567", sdf.parse("17-06-1987"), "AD_PRES", 24000, -1, -1, 90));

result.add(new Employee(101, "Neena", "Kochhar", "NKOCHHAR", "515.123.4568", sdf.parse("21-09-1989"), "AD_VP", 17000, -1, 100, 90));

result.add(new Employee(102, "Lex", "De Haan", "LDEHAAN", "515.123.4569", sdf.parse("13-01-1993"), "AD_VP", 17000, -1, 100, 90));

} catch (Exception e) {

return new ArrayList<Employee> ();

}

return result;

}

}

For this blog the result of the web service is hard coded.

The @Path annotation above the class definition contains a warning:



If you click on this warning a jersey servlet is added to the web.xml:

<servlet>

<servlet-name>jersey</servlet-name>

<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>jersey</servlet-name>

<url-pattern>/jersey/*</url-pattern>

</servlet-mapping>


Step 7

Now the EmployeeService can be created so it can be deployed.

Right click in the Application Navigator on the EmployeeService java class and choose Create Web Service:
  • Give the web service a name for example EmployeeWebService
  • Choose SOAP 1.1 Binding
  • Leave all other options to their default value
Between the @Path annotation and the class definition a new line is added:

@WebService(name = "EmployeesWeb", serviceName = "EmployeesWebService", portName = "EmployeesWebPort")

And in the web.xml is added:

<servlet>

<servlet-name>EmployeesWebPort</servlet-name>

<servlet-class>nl.hr.demo.webservices.EmployeesServices</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>EmployeesWebPort</servlet-name>

<url-pattern>/EmployeesWebPort</url-pattern>

</servlet-mapping>


Step 8

Deploy the application as a EAR file to the weblogic server:



If you click on the Service and go to the testing tab you can test the webservice:



If we test getEmployeeById and query id 101 we get:



With result:



And test getAllEmployees results in:

maandag 28 februari 2011

How to add a context menu in a table

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 ADF11 it is possible to create context menus in a table. This is a menu that can be opened by right clicking on a table row.

In this blog is described how to add a context menu in a table.

Overview of the context menu:



Setup example application



For this blog an example application is created based on the HR schema. The example application contains an employees table with 2 detail forms, one to insert and edit an employee without department) and 1 to change the department of the employee. The detail forms are created in separate pages. For the employees table and forms a bounded task flow is created this bounded task flow is started from the menu.


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.



Task flow




Unbounded task flow



The unbounded task flow form where we start with the solution looks like this:



There are no customizations made, the task flow is created by drag and drop.

Bounded task flow



The employees task flow is a bounded task flows:



The next properties are set:
Property Value
usePageFragments false

Share data controls with calling task flow true




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 Row Selection and Sorting checked, all columns are displayed.





The Row Selection property must be checked, this causes the following properties to be set in the table:
Property Value
selectedRowKeys #{bindings.EmployeesView.collectionModel.selectedRow}
selectionListener #{bindings.EmployeesView.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.

Form pages



The form pages are created by drag and drop from the Data Controls. The form is dropped as ADF Form.



To navigate back from the form to the table a rollback button (af:commandButton) is added by drag and drop the Rollback operation from the datacontrol palette as a button.



The submit button is created in the same way but then from the Commit operation.

The edit and insert employee page contains all fields but with department ID readOnly. The change department page contains the employee ID, first name, last name and department ID. All fields except department ID are read only.

Add the context menu



An ADF table contains a facet named contextMenu. With this facet a menu popup can be created that pops up when the user right clicks on a table row. If the table contains no rows the context menu does not popup on right click.

Insert in the table the contextMenu facet. This facet should contain a popup and in this popup a menu can be defined. Submenu entries can be created by inserting another menu tag in the menu:

<f:facet name="contextMenu">

<af:popup id="popup">

<af:menu id="menu">

<af:menu id="employeeMenu"

text="Employee">

<af:commandMenuItem text="Edit"

id="editEmployeeMenu"

action="edit"/>

<af:commandMenuItem text="Insert"

id="insertEmployeeMenu"

action="edit"

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

</af:menu>

<af:menu id="departmentMenu"

text="Department">

<af:commandMenuItem text="Change department for employee"

id="changeMenu"

action="change"/>

</af:menu>

<af:commandMenuItem text="Cancel"

id="cancelMenu"

action="cancel"/>

</af:menu>

</af:popup>

</f:facet>

The insert menu option is created by drag and drop the CreateInsert operation of the EmployeesView, the action is overridden so it navigates to the same page as edit employee:



When the user for example choses Change department for employee:



This results in:

zondag 27 februari 2011

How to control the tab page icon and text

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: none


Summary



In this blog a solution is provided how to control the tab page icon and text of a browser. The solution of changing the icon does not work in internet explorer.

Bij default a page looks like this:



The text deptOverview is set in the af:document tag:

<af:document id="d1" title="deptOverview">

The picture cannot be changed in this tag. To change the picture the head information of the generated page must be changed. This can be done using the metaContainer facet:

Add:

<f:facet name="metaContainer">
<f:verbatim>

<link type="image/x-icon" href="../../common/images/initials.png" rel="shortcut icon"/>

</f:verbatim>

</f:facet>

Just before the closing that of af:document.

Some notes about the picture:
  • It’s name must be lowercase
  • The size must be 16 by 16 pixels or an multiple of that
For firefox this works it looks like this:



For Internet explorer it doesn’t work. What did I try to make it work in IE:
  • Save the picture as a ico format
  • Set the URL of the picture fixed
  • Set the picture in the root
If anyone knows how to make it work in ADF11 in Internet explorer please let me know.

vrijdag 25 februari 2011

How to trigger an action on enter

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 a solution is provided how to trigger an action on enter. An action can be a command link or button. All defined actions (action, actionListener, showPopupBehaviour) will be executed when the user uses the enter.

To trigger an action set the defaultCommand property of the af:form tag. Set the value to the ID of the button / link that should be triggered on action.

We’ll use the following page in this blog:



This page is based on the EMPLOYEES table in the HR schema. The table is created by drag and drop from the Data control palette. An edit (commandLink) column is added to navigate to a form of the employee. The page contains 2 buttons, a Cancel which navigates back to the home page and create an insert which creates a now employee row and navigates to the form.

Trigger a button



In this example we’ll trigger the Cancel button, the cancel button asks for confirmation and if the user answers with OK it navigates back to the home page.

The button looks like this:

<af:commandButton text="Cancel"

id="cancel"

immediate="true">

<af:resetActionListener/>

<af:showPopupBehavior popupId="cancelPopup"

triggerType="action"/>

</af:commandButton>

Set the default command of the af:form tag to this ID. Change:

<af:form id="f1">

To:

<af:form id="f1" defaultCommand="pt1:cancel">

Where pt1 is the id of the page template that contains the cancel button.

When we run this and we press enter you get:



Trigger a link



In this example we’ll trigger the Edit command link, the link contains an actionListener which adds an information message to the stack and an action which navigates to the form.

First set the current row of the view object by selecting a row:



Then when enter is pressed:



The commandLink for this example looks like this:

<af:column headerText="Edit employee"

id="c12">

<af:commandLink text="Edit"

id="cl5"

actionListener="#{employeeBean.edit}"

action="edit"/>

</af:column>

The default command of the af:form tag is set to:

<af:form id="f1" defaultCommand="pt1:t1:cl5">

Where pt1 is the id of the page template and t1 is the id of the table that contains the command link column.

The employeeBean refers to a request scoped bean that contains an edit method:

public void edit(ActionEvent actionEvent) {

EmployeesViewImpl view = getService().getEmployeesView();

String name = view.getCurrentRow().getAttribute("FirstName") + " " + view.getCurrentRow().getAttribute("LastName");

FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Edit", "Edit person: " + name.trim()));

}


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();

}

How to set the cursor on page opening

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 a solution is provided how to set the cursor in a specific field on page opening.

When a page with input fields is created by drag and drop from the data control palette it looks like this:



The user first has to click in a field before he can edit it.

But if we add into the af:document tag an initialFocusId clause referring to the component that should be active and set in that component the clientComponent property then the cursor is in that field on page opening.

In this example we changed:

<af:document id="d1"

title="emp">

To:

<af:document id="d1"

title="emp"

initialFocusId="pt1:it4">

With pt1:it4 is the id of the inputText EmployeeId (id it4) in the page template (id pt1).

We changed the input text employee ID from:

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

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

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

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

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

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

id="it4">

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

<af:convertNumber groupingUsed="false"

pattern="#{bindings.EmployeeId.format}"/>

</af:inputText>

To:

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

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

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

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

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

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

id="it4"

clientComponent="true">

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

<af:convertNumber groupingUsed="false"

pattern="#{bindings.EmployeeId.format}"/>

</af:inputText>

Now the page looks on opening like this:



The initialFocus clause can also be conditional.

For example, on creation of a new employee the cursor must be in employee ID, when an existing employee is edit the employee ID is not updatable and the cursor must be in firstname.

When the employee is created the employee ID is blank, when an employee is edited it’s filled. This fact is used to render the inialFocus. Change it to:

<af:document id="d1"

title="emp"

initialFocusId="#{bindings.EmployeeId.inputValue==null?'pt1:it4':'pt1:it2'}">

Add a readOnly clause to the employee ID input text:

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

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

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

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

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

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

id="it4"

readOnly="#{bindings.EmployeeId.inputValue!=null}"

clientComponent="true">

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

<af:convertNumber groupingUsed="false"

pattern="#{bindings.EmployeeId.format}"/>

</af:inputText>

And set the clientComponent property to true for the first name input text (id it2):

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

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

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

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

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

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

id="it2"

clientComponent="true">

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

</af:inputText>

Now the page looks for opening in insert or edit mode like this:

donderdag 24 februari 2011

How to never display a horizontal scrollbar in a table

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



Display an table layout in ADF11 is easy, but I’m annoyed by the horizontal scrollbar in the button of the table. A vertical scrollbar I can imagine, I believe it’s more user friendly then the page range that ADF10 had.

Let’s first see what ADF11 creates when dragging and dropping the Employees table from the data control palette. When the browser has a ‘reasonable’ size it looks like this:



Why is the table that small? The screen is so big I would prefer the table being wider so the vertical scrollbar can disappear. When we make the screen (very) small it’s even worse:




Now we have to scrollbars. The scrollbar underneath the Cancel button is just to make the vertical scrollbar of the table visible. But actually this scrollbar (the one underneath the Cancel button) is the one I would like to keep it’s the scrollbar of the complete page content (like word has a scrollbar if you make the screen smaller then a page).

This issues can easily be solved. Just put in the table:

styleClass="AFStretchWidth"

This is equal to width 100%. Then if we have a wide screen it looks like this:



But know I wonder, why such a use gap between the last column and the vertical scrollbar? If we make the screen small it looks like this:



Now we only have one scrollbar, but if we would add items underneath the table wider as the screen a second horizontal scrollbar will be displayed at the bottom of the page (content so above the Marianne Horsch © ADF footer).

So still I’m not really satisfied by how the table looks. Now look at this wide screen:



That looks fine! Scrollbar just after the last column just where I would expect it. But in a small screen??



Also just like I would like it, at most one horizontal scrollbar but not in the table but at the bottom of the page content.

But how did I achieve this? Well I set the width instead of the styleClass. To be precise, I set the width to 1276.

Calculate the width of the table



You may think 1276, how did she come up with that number? Trial and error? And does it really has to be exactly 1276?

First answer the second question:



This picture is width 1280, there is now a gap between the last column and the scrollbar. So yes it needs to be exactly 1276 (for this table).

Then the first question, was it trial and error? No it wasn’t, you can calculate the width of the table. And it is as follows:
  • Sum all the widths of the columns
  • Add to this the number of columns multiplied by 5
  • If the table has a vertical scrollbar then add 16
In this example this is:
  • Sum of all width of the columns is 1200 (12 columns each with width 100)
  • 12 columns multiplied by 5 is 60
  • We have a scrollbar so add 16
  • Makes total 1200 + 60 + 16 = 1276.

A generic solution



Of course, you don’t want to recalculate the width of the table every time you add or remove a column, or what if columns are rendered based on criteria such as user roles? And display a scrollbar or not is not fixed as well.

Before the solution is described let us first take a look at the example application:

Setup example application



For this blog an example application is created based on the HR schema. The example application contains an employees table with a filter. For this employees table a bounded task flow is created which is started from the menu.

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 Added 2 bind variables:
  • b_min_salary (Number)
  • b_min_salary (Number)
Changed where clause:

(:b_min_salary IS NULL OR (:b_min_salary IS NOT NULL AND Employee.SALARY >= :b_min_salary)) AND (:b_max_salary IS NULL OR (:b_max_salary IS NOT NULL AND Employee.SALARY <= :b_max_salary))


Create an application module HrAppModule which exposes the EmployeesView.



Unbounded task flow



The unbounded task flow from where we start with the solution looks like this:



There are no customizations made, the task flow is created by drag and drop.

Bounded task flow



The employees task flow is a bounded task flows.



The next properties are set (for both bounded task flows):

Property Value
usePageFragments false

Share data controls with calling task flow true


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 Row Selection and Sorting checked, all columns are displayed.





Underneath the table a Cancel button (af:commandButton) is added which ends the task flow.

af:commandButton property Value
text Cancel
id cancel
action cancel
immediate true


Above the table a panelFormLayout is added that contains 2 input text items and a button.

The input text items have the following properties:

Property First input text Second input text
binding #{employeeBean.minSalaryFilter} #{employeeBean.maxSalaryFilter}
label Minimum salary Maximum salary
id minSalaryFilter maxSalaryFilter
f:validator binding #{bindings.Salary.validator} #{bindings.Salary.validator}
af:convertNumber groupingUsed false false
af:convertNumber pattern #{bindings.Salary.format} #{bindings.Salary.format}


The af:commandButton has the following properties:

Property Value
text Filter
id filter
actionListener #{employeeBean.filter}


Add to the page definition of the table page inside the bindings tag:

<attributeValues IterBinding="EmployeesViewIterator"

id="Salary">

<AttrNames>

<Item Value="Salary"/>

</AttrNames>

</attributeValues>

Set the partialTrigger property of the table:

partialTriggers="::filter"


Employee bean



In the table page references are made to the employeeBean. This bean class is defined in the employee task flow:

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


The bean class contains two class variables RichInputText minSalaryFilter and RichInputText maxSalaryFilter and their accessors.

The bean class also contains the actionListener implementation. In this implementation the EmployeesView is retrieved from the application module and the values of minSalaryFilter and maxSalaryFilter are copied to the EmployeesView and the view is queried:

public void filter (ActionEvent actionEvent) {

EmployeesViewImpl view = getService().getEmployeesView();

view.setb_min_salary(getValue(minSalaryFilter));

view.setb_max_salary(getValue(maxSalaryFilter));

view.executeQuery();

}



private Number getMValue(RichInputText item) {

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

return null;

}

return new Number(((BigDecimal) 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();

}

The class uses the next imported classes:

Class name Package
BigDecimal java.math
FacesContext javax.faces.context
ActionEvent javax.faces.event
DCBindingContainer oracle.adf.model.binding
DCDataControl oracle.adf.model.binding
RichInputText oracle.adf.view.rich.component.rich.input
BindingContainer oracle.binding
Number oracle.jbo.domain

Fix the table width



Now we’re ready to fix the table width with a generic solution. For this solution we need:
  • A bean class that binds the table and calculates the width (in or exclusive scrollbar)
  • Set properties in the table to use the bean class

Bean class



Define in the unbounded task flow a new bean:

Managed bean property Value
Name tableBean
Class nl.hr.demo.view.util.TableBean
Scope Request


The bean class contains a class variables RichTable table and its accessors.

The bean class contains 1 method which returns the width of the table as an int. The width is calculated by:
  • Loop over all columns of the table:
    • If the column is rendered and visible then add its width to a local integer and add 1 to another local integer that counts the number of columns.
  • Add to the width the number of columns multiplied by 5.
  • Retrieve the number of rows in the view object that is displayed by the table:
    • Get the value property of the table cast it to a CollectionModel and get the estimatedRowCount value.
  • If the number of rows is bigger than the number of rows displayed in the table (the autoHeightRows property of the table) then add 16 to the width.
  • Return the width.

public int getWidth() {

if (table == null) {

return 600;

}

try {

int width = 0;

int columns = 0;

List list = table.getChildren();

for (int i = 0; i < list.size(); i++) {

UIComponent component = list.get(i);

if (component instanceof RichColumn) {

RichColumn column = (RichColumn) component;

if (column.isRendered() && column.isVisible()) {

width += new Integer(column.getWidth()).intValue();

columns++;

}

}

}

width += (columns * 5);

int nrRows = 0;

if (table.getValue() != null) {

CollectionModel tableData = (CollectionModel) table.getValue();

nrRows = tableData.getEstimatedRowCount();

}

if (nrRows > table.getAutoHeightRows()) {

width += 16;

}

return width;

} catch (Exception e) {

e.printStackTrace();

return 600;

}

}

The class uses the next imported classes:

Class name Package
List java.util
UIComponent javax.faces.component
RichColumn oracle.adf.view.rich.component.rich.data
RichTable oracle.adf.view.rich.component.rich.data
CollectionModel org.apache.myfaces.trinidad.model

Table properties



Set the following properties of the employees table:

Property Value Description
binding #{tableBean.table} Binds the table to the RichTable class variable of the TableBean
width #{tableBean.width} The width is set to the result of the getWidth method of the TableBean
autoHeightRows 10 The (maximum) number of rows displayed in the screen
contentDelivery immediate Must be set to immediate otherwise the autoHeightRows property doesn’t work


Note: Firefox displays one row more than set in the autoHeightRows property . When the autoHeightRows property is set to 10 and there 11 rows in the view object the table bean adds space for a scrollbar but because firefox displays 1 row more the scrollbar space is empty.

Too avoid this a small correction should be made to the TableBean (only tested in IE and Firefox):

Change:

if (nrRows > table.getAutoHeightRows()) {

To:

if (nrRows > (table.getAutoHeightRows() + browserCorrectionRowsDisplayed())) {

The implementation of the browserCorrectionRowsDisplayed method:

public int browserCorrectionRowsDisplayed() {

String browser = (RequestContext.getCurrentInstance()).getAgent().getAgentName();

if (browser != null && browser.equalsIgnoreCase("gecko")) {

return 1;

}

return 0;

}

The result: