Since the requirement on how to effectively skin a custom WebCenter Portal application frequently becomes a major topic in many of my customer egagements, I have decided to posts a series of blogs that will provide some insight on skinning some of the out-of-the-box (OOTB) ADF components. The first component skinning example that I will cover is the ADF table.
Most of the time when I see a custom skin implementation for a table, the code for the table is actually made up of a main header, proceeded byan iteration-type component - af:forEach or af:iterator - then proceeded by the row(s). The actual row would be constructed as a container, which can hold other containers, that holds the record items. This approach does make skinning the table much simpler, but there can be more components needed to construct the table itself. Moreover, some of the OOTB af:table behavior, for example: column sorting, and filtering to name a few, will have to implemented as custom code.
The image below is how the sample table looks without any custom skining:
The image below is example of the same af:table with a custom skin. Beyond the obvious look-n-feel (LnF) difference of a non-skinned table, there is also the addition of a custom pagination feature. While the (default) scroll-bar behavior maybe acceptable for a web-basedapplication, it is not a desirable feature for a typical web-site portal design. In addition, in my experiences with customers on the WebCenter platform, the custom pagination behavior is one of the most requested requirements.
Skinning the af:table
Skinning the af:table is not a difficult process. Knowing the correct style selectors to use is usually the most challenging part. The following are a few tips that you can do to help identify the correct style selectors:
- In the web.xml, set the <context-param> for the org.apache.myfaces.trinidad.DISABLE_CONTENT_COMPRESSION to true
- Use a tool like Firefox's FireBug, or the Developer Tools for Internet Explorer
- Since WebCenter's version of JDeveloper does come (yet) with the ADF Skin Editor, download the standalone version here
- The information of the style selectors for the af:table is available in the documentation
For my table example, these are the style selectors that I used. Please note that this example focuses only on the style selectors and not on the CSS elements (i.e. background-color, .etc).
af|table{
border:none; // removes outer border
height:231px; // scaled to fit the max number of rows return from pagination
}
af|table af|column::column-header-cell{
color:#696969;
background-color: #D9D9D9;
font-size:11px;
height:30px;
border:white 1px solid; // the grid lines
}
af|table::data-table-VH-lines af|column::data-cell{
background-color:#F1F1F1;
height:35px;
font-size:11px;
border:white 1px solid; // grid lines
padding-right:0px;
}
Note: Inserting an icon within the header requires an additional style class (headerClass) property attribute to the af:table. This addition can be done in the af:table's Property Inspector.
af|table af|column::column-header-cell.headerIcon{
background-image:url('/oracle/webcenter/portalapp/shared/images/base/Tooltip.png');
background-position:75px 8px;
background-repeat: no-repeat;
}
This styles the content for both the unit and charge amount. The two af|columns have been tagged with a custom style class.
af|table::data-table-VH-lines af|column::data-cell.boldcols{
font-weight:bold;
text-align: right;
}
af|table::data-row:Highlighted af|column::data-cell{
background-color: #FFE6E6;
}
af|table::data-row:Selected:Focused af|column::data-cell{
background-color: #C9131B;
font-weight:bold;
color:white;
}
The pagination bar contains OOTB af:commandLinks. The links have been tagged with a custom style class and the selectors used are as follows:
af|commandLink.pageNums{
color:#C9131B;
font-size: 12px;
}
af|commandLink.selectedPageNums{
font-weight:bold;
font-size: 14px;
color:Black;
}
Next and Previous Button...
af|commandLink.navbuttons{
color:#C9131B;
font-weight:bold;
white-space:nowrap;
}
af|commandLink.navbuttons:disabled{
visibility: hidden;
}
af|table.custTable::data-table-VH-lines af|column::data-cell.boldcols{
font-weight:bold;
text-align: right;
}
As you can see there is not much that you have to do to customize the LnF for the OOTB af:table. This type of customization is recommended, since everything that has been done is based on OOTB components and CSS standards.
Implementing the custom pagination support
Before I begin the explanation on how to acheive the pagination, I would like to note that this is custom code. However, I have only used OOTB ADF components, ADF-based APIs, and more important, I have not used any javascript, which could interfere with the ADF runtime.
The first item that needs to be configured is the af:table itself. By default, this table's value is set to table's tree binding CollectionModel . However, since I only want to see at most the amount set by the Range Size configured in the page definiton file, this value is set to the rangeSet. The following is a fragment of the af:table, which examplifies the configuration. In addition, note the autoHeightRows property is set to the rangeSize as well.
af:table value="#{bindings.DemoDataTypes.rangeSet}" var="row"
rows="#{bindings.DemoDataTypes.rangeSize}"
emptyText="#{bindings.DemoDataTypes.viewable ? 'No data to display.' : 'Access Denied.'}"
fetchSize="#{bindings.DemoDataTypes.rangeSize}"
rowBandingInterval="0" id="t1" styleClass="custTable"
width="760px;"
autoHeightRows="#{bindings.DemoDataTypes.rangeSize}"
partialTriggers="::cb1 ::cb2" rowSelection="single">
The pagination support is achieved through the ADF page tags that are wired to custom methods in a managed bean. The following is the code fragment that contructs the pagination bar:
<af:commandLink
text="<< Previous"
disabled="#{!bindings.PreviousSet.enabled}"
partialSubmit="true" id="cb2"
styleClass="navbuttons"
action="#{pageFlowScope.tablehelper.previousSet}"
/>
// The commandLink will invoke an actionListener method and will set in the managed bean the desired page to navigate to
<af:forEach items="#{pageFlowScope.tablehelper.numberOfPages}"
var="pages">
<af:commandLink id="clickedvalue" text="#{pages}"
actionListener="#{pageFlowScope.tablehelper.selectedPage}"
styleClass="#{pageFlowScope.tablehelper.pageNumber eq pages ? 'pageNums selectedPageNums' : 'pageNums'}">
<af:setPropertyListener from="#{pages}"
to="#{pageFlowScope.tablehelper.pageNumber}"
type="action"/>
</af:commandLink>
</af:forEach>
<af:commandLink
text="Next >>"
disabled="#{!bindings.NextSet.enabled}"
partialSubmit="true" id="cb1"
styleClass="navbuttons"
action="#{pageFlowScope.tablehelper.NextSet}"
/>
Both the Previous and Next links are wired to the appropriate ADF operation bindings that control the iteration:
public String previousSet() {
BindingContainer bindings = getBindings();
OperationBinding operationBinding = bindings.getOperationBinding("PreviousSet");
Object result = operationBinding.execute();
if (!operationBinding.getErrors().isEmpty()) {
return null;
}
int newPage = getPageNumber()-1;
setPageNumber(newPage);
calculateRowsLeft(newPage);
return null;
}
public String NextSet() {
BindingContainer bindings = getBindings();
OperationBinding operationBinding = bindings.getOperationBinding("NextSet");
Object result = operationBinding.execute();
if (!operationBinding.getErrors().isEmpty()) {
return null;
}
int newPage = getPageNumber()+1;
setPageNumber(newPage);
calculateRowsLeft(newPage);
return null;
}
Since the pagination page numbers are dynamic, they are calculated when the <af:forEach items="#{pageFlowScope.tablehelper.numberOfPages}"
is evaluated in the managed bean:
private int RANGE_SIZE = getTableView().getRangeSize();
private List<Integer> numberOfPages = new ArrayList<Integer>();
public long calculatePages() {
JUCtrlRangeBinding tableView = this.getTableView();
long numrows = tableView.getEstimatedRowCount();
long pages = numrows / RANGE_SIZE;
long remainder = numrows % RANGE_SIZE;
long totalpages = 0;
if (remainder > 0) {
totalpages = pages + 1;
} else {
totalpages = pages;
}
return totalpages;
}
// This create the pages
public void createPageList() {
long index = getContentpages();
for (int i = 1; i <= index; i++) {
getNumberOfPages().add(new Integer(i));
}
}
The access to the table binding is achieved with the following code:
private JUCtrlRangeBinding getTableView() {
BindingContext bindingCtx = BindingContext.getCurrent();
BindingContainer bindings = bindingCtx.getCurrentBindingsEntry();
return (JUCtrlRangeBinding)bindings.getControlBinding("DemoDataTypes");
}
The pagination links are using the selectedPage method. The magic to the navigation is through the RowSetIterator in the getItemsInPage method:
public void selectedPage(ActionEvent actionEvent) {
int page = getPageNumber();
getItemsInPage(page);
calculateRowsLeft(page);
}
public void getItemsInPage(int range) {
JUCtrlRangeBinding tableView = this.getTableView();
RowSetIterator rsi = tableView.getIteratorBinding().getRowSetIterator();
rsi.scrollToRangePage(range);
}
Finally, the pagination bar also contains information about the remaing row(s) that have yet to be paginationated to. For example, there are a total of 16 records. On the first set (page 1), the View more results would then show: 16-5 (range set) = 11. Page 4, would only show 1 row in the table, and View more results would be 0. The method resposible for the calculation:
public void calculateRowsLeft(int page){
long result;
if(getContentpages() > page){
result = totalRows - (page * RANGE_SIZE) ;
}else{
result = 0;
}
setRowsLeft(result);
}
Well incredibly enough that is all that is needed to support custom pagination for the OOTB af:table. The source code that I have used in this blog is here.