JDeveloper: Advanced Skin Technique

This post is about an advanced technique to change the look and feel of an ADF application. Changes to the look & feel are normally done via a skin which you use to change descriptors which are used by the ADF components. The general technique to do this is described in many blogs and articles like ADF Faces Skin Editor – How to Work with It and the official documentation at Oracle ADF Skin Editor.

In this blog we look at an advanced technique which helps to change the look and feel of components like af:query and pf:panelCollection which you can’t change using the normal available descriptors. In the below image you see the Skin Editor showing the ADF components skin descriptors.

selection_985

Use Case

In this use case we work with the af:panelCollection component. This component is used to wrap af:tree, af:treeTable and af:table components to provide additional functions. From the documentation of af:panelCollection

A panel component that aggregates collection components like table, treeTable and tree to display standard/application menus, toolbars and statusbar items.

The default top level menu and toolbar items vary depending on the component used as the child of the panelCollection.

  • For table, tree and treeTable, the default top level menu item is View.
  • For table and treeTable with selectable columns, the default top level menu items are View and Format.
  • For table and treeTable, the default toolbar item is Detach.
  • For table and treeTable with selectable columns, the default top level toolbar items are Freeze, Detach and Wrap.
  • For tree and treeTable, if the pathStamp facet is used, the toolbar buttons Go Up, Go To Top, Show as Top also appear.

The component allows us to switch off some function

Value Turns off
statusBar Status bar
viewMenu ‘View’ menu
formatMenu ‘Format’ menu
columnsMenuItem ‘Columns’ sub-menu item
columnsMenuItem:col1,col20 Columns with column ID: ‘col1’ and ‘col20’ inside ‘Columns’ sub-menu
freezeMenuItem ‘Freeze’ menu item
detachMenuItem ‘Detach’ menu item
sortMenuItem ‘Sort’ menu item
reorderColumnsMenuItem ‘Reorder Columns’ menu item
resizeColumnsMenuItem ‘Resize Columns’ menu item
wrapMenuItem ‘Wrap’ menu item
showAsTopMenuItem Tree/TreeTable ‘Show As Top’ menu item
scrollToFirstMenuItem Tree/TreeTable ‘Scroll To First’ menu item
scrollToLastMenuItem Tree/TreeTable ‘Scroll To Last’ menu item
freezeToolbarItem ‘Freeze’ toolbar item
detachToolbarItem ‘Detach’ toolbar item
wrapToolbarItem ‘Wrap’ toolbar item
showAsTopToolbarItem Tree/TreeTable ‘Show As Top’ toolbar item
wrap ‘Wrap’ menu and toolbar items
freeze ‘Freeze’ menu and toolbar items
detach ‘Detach’ menu and toolbar items

As a sample the image below shows a normal af:panelCollection (upper half) and an af:panelCollection with the view menu and the toolbar switched off (lower half)

selection_979

Looking at the possible things to switch off we don’t see anything to switch off the ‘Query by Example’ (QBE) icon. There is no feature toggle to turn this function on or off. An easy way to get rid of the icon would be to make the table not filterable. However, if we like the table to be filterable but don’t want to show the icon to switch the feature off, we have to use an advanced skin technique.

What can we do to get rid of the icon in the tool bar?

The idea is to use a skin or special css to hide the icon or the container which holds the icon. To find the container we first inspect the page in the browser using the browsers ‘Developer Tools’ which you can reach by hitting F12 in your browser. Below you see Chrome 55 with activated ‘Developer Tools’

Selection_211.jpg

The image shows the toolbars QBE image as selected element on the page (left red rectangle) and the style classes which are in use for this element (right red rectangle). The names ‘.xfo’ and ‘.xfr’ are the names of the style classes. They are minimized to reduce the download size of the page, but they are not ‘readable’. 

The first thing to do is to make the names ‘readable’ for us. We need to know which skin selector generated the style class. For this we set a context parameter in the web.xml file

 <context-param>
 <param-name>org.apache.myfaces.trinidad.DISABLE_CONTENT_COMPRESSION</param-name>
 <param-value>true</param-value>
 </context-param>

Setting this parameter to true will show us the clear names. The image below shows the same selection only this time with the real names

selection_212

One other nice feature of the ‘Developer Tools’ is that you can inspect elements by just hover over them on the page. This allow us to easily find the element we want to hide via css. Click on the icon marked in hte below image

selection_213

and move the mouse cursor over the page. You see the HTML and the active styles of the element under the cursor. This feature we use to find an element which holds the icon we want to hide and which we can address via css .

selection_214

CSS allows us to address elements inside a skin selector.  For this you need to know the skin selector, the tag or container and it’s ID inside the selector you want to address. In the image above we see the ID of the icon container we want to hide as “id=’pc1:_qbeTbr'” and the container or tag itself which is a ‘div’. The skin selector is the af|panelCollection. With this information we can can change the style attached to the ‘div’ container with the id ‘*_qbeTbr’  in the af|panelCollection as

af|panelCollection div[id$='_qbeTbr'] {
    display: none;
}

This we can add to our skin.css file. However, if we just add it this way it’s changing all af:panelCollection in our application.  If we want this only to be active for specific af:panelColletion we can add a style class name like

af|panelCollection.myPCClass div[id$='_qbeTbr'] {
    display: none;
}

Now we can add the stale class name ‘myPCClass’ to the af:panelCollection when we like the QBE icon not to be shown

 <af:panelCollection id="pc1" styleClass="myPCClass">
   <f:facet name="menus"/>
   <f:facet name="toolbar"/>
   <af:table value="#{bindings.EmployeesView1.collectionModel}" ...
   ...
 <af:panelCollection id="pc2" featuresOff="detachToolbarItem viewMenu">
 <f:facet name="menus"/>
 <f:facet name="toolbar"/>
 ...

will generate this UI output

selection_217

 

As we see, the QBE icon is gone. In the original page we have placed two af:panelCollection components. As you added the new style class only to one of them, the other QBE icon is still visible.

Extending

You can use hte same technique for other complex ADF components like af:query. Here you can style the save button which normally not  supported.

Download

You can download the sample which is build using JDev 12.2.1.2.0  and uses the HR DB schema from GitHub BlogAdvancedSkin

Undo Reorder of Columns in af:table

A question on OTN about how to undo a reorder of columns in an af:table can be undone. In this blog I show how to undo such a reorder to show the columns of an af:table in their natural order.
The natural order is defined when you create the table. You can move the attributes in the create dialog or delete attributes you don’t want to see in the UI from the table.

In the image above we see the dialog after we drop a VO as table onto a page. To change is order of the columns in the table you can use the arrows on the right (in the red rectangle). Once you save the table you can reorder the columns in the property editor of the af:table.

img00003

The order of the columns you see in the dialog or the property editor is what is called default order of the columns. This default order can be different than the order of the attributes in the query the VO is based on.
The page we drop the af:table on is very simple. It is build from a quick layout and has a header for the page title and a panelCollection which holds the table.

img00008

We can reorder the columns in the UI by dragging a column and dropping it at a different location.

The question now is how to undo this manual reorder without refreshing the browser window.

To understand how this is implemented, we need to look how the the reorder is done in the first place. A table is build from one or more columns. Each of the columns describes the data to be shown in the column, the header to show and the display index which is the order of the columns shown in the UI. If the display index is less then zero (e.g. -1) the default order is used. Any other positive number is used to show the columns in ascending order of these display index.
To undo any reorder of the columns is an af:table we simply have to get to each column and set it’s display index to -1.

public class UndoColumnReorderBean {
    private static ADFLogger _logger = ADFLogger.createADFLogger(UndoColumnReorderBean.class);
    private RichTable table;

    public UndoColumnReorderBean() {
    }

    public void undoColunmReorder(ActionEvent actionEvent) {
        _logger.info("Undo reorder...");
        // get the tables child components
        List<UIComponent> children = this.table.getChildren();
        for (UIComponent comp : children) {
            // check if the child is a column
            if (comp instanceof RichColumn) {
                RichColumn col = (RichColumn) comp;
                // if hte display index is greater 0 set it to -1
                if (col.getDisplayIndex() >= 0) {
                    _logger.info("...unset column "+col);
                    col.setDisplayIndex(-1);
                }
            }
        }
        _logger.info("... done!");
    }

    public void setTable(RichTable table) {
        this.table = table;
    }

    public RichTable getTable() {
        return table;
    }
}

The bean above has a method undoColumnReorder which is an action event Listener triggered by clicking the ‘Undo Column Reorder’ button. This method uses the af:table component which is bound to the bean as property. It iterates over the child components of the table, checking if the child is a RichColumn (or af:column in the UI) and if yes sets its display index to -1;
To show the change in the UI, we have to ppr the table by adding the button as partial Trigger to the table

img00007

After clicking the button in the ui the table again looks like

img00004

so the default order of the columns is shown again.

You can download the application from GitHub BlogUndoColumnReorder. The sample is build using JDev 12.2.1.2 but you can do the same with any other JDev version 11g or 12c you use. It uses the HR DB schema.

Reset Table Filter when Navigating to Page

This blog is a continuation of an older blog about how to reset the filters of an af:table component from a bean (How to reset a filter on an af:table the 12c way). In the older blog I described the technique to reset the filters defined in the FilterableQueryDescriptor of a filterable af:table.

Now users on OTN JDev & ADF space ask for a small variation of the use case. The filter should reset whenever a navigation takes place to the page which holds the af:table. No button should be clicked to reset the filter values.

As the original technique can still be used, I don’t go into detail about how to do this. It’s described in the other blog for JDev versions 12c. The same technique can be applied to 11g but different Java code has to be used (see How to reset a filter on an af:table). I changed the sample application, which you can download (see link at the end of the blog), so that the query panel with the af:table has an additional button to navigate to a different page.

Run through

After starting the application we see the page with an empty table as no search was done. Clicking hte search button will give us

selection_910

The ‘Navigate’ button simply navigate to another view which holds twu buttons which let you navigate back to the original page.

selection_911

The ‘back without clear filter’ just navigates back to the page, whereas the ‘back with clear filter’ navigates to a method in the task-flow which prepares the af:table for reset. This is the bounded task flow:

selection_912

The EmpQueryPanel holds the af:query with the result table as shown in the first image. The view is marked as default activity in the task flow. When you first run the application (page RTFQPTest.jsf) the task flow is added as region to the page showing the query panel with the result table.

When you hit the search button on the page the table shows all employees. Now we can filter the results like ‘FirstName’ contain ‘s’ and ‘LastName’ contains ‘k’

selection_913

Now if we hit the ‘Navigate’ button we go to the page shown in image 2 with the two buttons. If we click on hte ‘back without clear filter’ we come back to the page as shown above. The filter values are still present!

If we click on the ‘back with clear filter’ we see

selection_914

so the filter values are cleared. So, how is it done?

Implementation

In the original sample we had a button which we used to trigger a method which get the FilterableQueryDescriptor from the table. This descriptor holds the filter values which are cleared by looping over all ConjunctionCriterion which are the filter values. Here is the full method for 12c

 /**
 * method to reset filter attributes on an af:table
 * @param actionEvent event which triggers the method
 */
 public void resetTableFilter12c(ActionEvent actionEvent) {
   FilterableQueryDescriptor queryDescriptor = (FilterableQueryDescriptor) getEmpTable().getFilterModel();
   if (queryDescriptor != null &amp;&amp; queryDescriptor.getFilterConjunctionCriterion() != null) {
     logger.info("Filter found...");
     ConjunctionCriterion cc = queryDescriptor.getFilterConjunctionCriterion();
     List&lt;Criterion&gt; lc = cc.getCriterionList();
     if (!lc.isEmpty()){
       logger.info("...iterating criterions...");
     }
     for (Criterion c : lc) {
       if (c instanceof AttributeCriterion) {
         AttributeCriterion ac = (AttributeCriterion) c;
         Object object = ac.getValue();
         logger.info("...found " + ac.getAttribute().getName() + " value: " + object);
         if (object != null) {
           ac.setValue(null);
           logger.info("...reset...");
         }
      }
   }
getEmpTable().queueEvent(new QueryEvent(getEmpTable(), queryDescriptor));
  }
}

public void setEmpTable(RichTable empTable) {
 this.empTable = empTable;
}

public RichTable getEmpTable() {
 return empTable;
}

A look into the log after clicking hte ‘back with clear flter’ shows

selection_915

We see that the for loop caught all filters and resetted every filter to null.

The interesting part is how we triggered the call of the method resetTableFilter12c. As there is no button or other action event involved we use a trick. We add a method to the ‘ShortDesc’ property of the af:table which points to a bean method

selection_916

Now, whenever the af:table is rendered it goes to the bean method asking for the test for hte short description. We use the call of this method as trigger to reset the filters. As this method is called multiple times during the JSF lifecycle, we need some kind of flag which tells us that the reset operation is done already. Otherwise we will spende lots of time calling the reset method without need.

public void setShortDescription(String shortDescritopn) {
logger.info("Set ShortDescription called");
this.shortDescription = shortDescritopn;
}

public String getShortDescription() {
logger.info("get ShortDescription called");
AdfFacesContext adfFacesCtx = AdfFacesContext.getCurrentInstance();

// get the PageFlowScope Params
Map<String, Object> scopePageFlowScopeVar = adfFacesCtx.getPageFlowScope();
Boolean reset = (Boolean) scopePageFlowScopeVar.getOrDefault("resetFilter", Boolean.FALSE);
boolean flip = reset.booleanValue();
if (flip) {
logger.info("ResetTable Filter!");
resetTableFilter12c(null);
scopePageFlowScopeVar.put("resetFilter", Boolean.FALSE);
logger.info("Unset filter reset flag!");
}

return shortDescription;
}

As there are cases where the short description is ask for which we don’t want to use as triggers to clear the filters, we need another flag which we can check. For this we set a flag in the pageFlowScope of hte bounded task flow named ‘resetFilter’.  in the method we get the pageFlowScope and read the flag (lines 8-13). Only when the flag is set to true in the pageFlowScope we call theresetTableFilter12c method (line 14-19) and reset the flag to false.

The only thing left to do is to set the flag in the pageFlowScope when we liek the filters to get cleared when navigating to the page. For this we use the method action ‘resetTableFilter’ which is defined in the task flow. This method action points to a bean method

selection_917

which puts the flag ‘resetFilter’ with a value of ‘Boolean.TRUE’ into the pageFlowScope:

public void setRestFlag() {
AdfFacesContext adfFacesCtx = AdfFacesContext.getCurrentInstance();
// get the PageFlowScope Params
Map<String, Object> scopePageFlowScopeVar = adfFacesCtx.getPageFlowScope();
scopePageFlowScopeVar.put("resetFilter", Boolean.TRUE);
logger.info("Set filter reset flag!");
}

Resources

You can download the sample application from GitHub:  BlogResetTableFilter12c

The sample uses JDev 12.2.1.2.0 and the HR DB schema.

Naviagting an af:table in pagination mode from a bean

A question on the JDeveloper and ADF OTN forum asked about how to navigate to a specific page of an af:table in pagination mode. As of JDeveloper 11.1.1.7.0 adf tables can be rendered in scroll mode or in pagination mode where only a specific number of rows are visible in the table.

af:table in pagination mode

To navigate the pages there is a small navigation toolbar below the table which allows to enter a page number or to navigate to the previous, next, first or last page.

The problem to solve is how to navigate the paginated table from within a java bean?

The table doesn’t offer any navigation listeners or methods you can bind bean methods to. Luckily there is the RangeChangeEvent one of the FacesEvents which can e used to notify a component that change in the range has taken place.

All we have to do to navigate the table in pagination mode is to calculate the needed parameters

  • oldStart: The previous start of this UIComponent’s selected range, inclusive
  • oldEnd: The previous end of this UIComponent’s selected range, exclusive
  • newStart: The new start of this UIComponent’s selected range, inclusive
  • newEnd: The new end of this UIComponent’s selected range, exclusive

We add an input field to the page which allow us to enter a page number and a button which we use to call an action listener in a bean.

The running application looks like

Running application

Another button is used to calculate the index of the selected row in the whole rowset, the index on the page and the page number. The row index and the index of the row on the page are zero based, page numbers start with 1. Let’s look at the code:

public void onGotoPage(ActionEvent actionEvent) {
BindingContainer bindingContainer = BindingContext.getCurrent().getCurrentBindingsEntry();
// get number of page to goto
AttributeBinding attr = (AttributeBinding) bindingContainer.getControlBinding("gotopage1");
Integer newPage = (Integer) attr.getInputValue();
if (newPage == null) {
return;
}
// page one starts at index 0 so subtract 1 from the pagen number
newPage--;
DCIteratorBinding iter = (DCIteratorBinding) bindingContainer.get("EmployeesView1Iterator");
// calculate the old and new rages for the RangeChangeEvent
int range = iter.getRangeSize(); // note both the table and we take the page size from the iterator's RangeSize
int oldStart = iter.getRangeStart();
int oldEnd = oldStart + range;
int newStart = newPage * range;
int newEnd = newStart + range;
// find the table
UIViewRoot iViewRoot = FacesContext.getCurrentInstance().getViewRoot();
UIComponent table = iViewRoot.findComponent("t1");
// build the event and fire it
RangeChangeEvent event = new RangeChangeEvent(table, oldStart, oldEnd, newStart, newEnd);
((RichTable)table).broadcast(event);
// update the table
AdfFacesContext.getCurrentInstance().addPartialTarget(table);
}

Line 2-8 we get the new page number we want to navigate to. Line 9-10 we subtract 1 from the given number as the page is zero based internally. In Line 11 we get the iterator which we need to get the range size and the start of the current range (lines 13-15). These values are oldStart and oldEnd. Lines 16-17 we calculate the new start range as page to go multiplied with the range. The newEnd parameter is the newStart pus the range size.
In lines 18-20 we get to the table component on the page. Then we create the RangeChangeEvent and broadcast the event to the table component in lines 21-23. Finally we ppr the table to see the change in the UI.

To show how to calculate the other way around, to get from the selected row in a table to the index on the page, the page number and the index in the rowset we added another button ‘GetPageOfSelectedRow’which calls a listener in the same bean which builds a string with the needed information.

public void onGetCurrentPage(ActionEvent actionEvent) {
BindingContainer bindingContainer = BindingContext.getCurrent().getCurrentBindingsEntry();
DCIteratorBinding iter = (DCIteratorBinding) bindingContainer.get("EmployeesView1Iterator");
// calculate index and page number. Index is zero based!
int currentRowIndex = iter.getRowSetIterator().getCurrentRowIndex();
_logger.info("CurrentRowIndex: " + currentRowIndex);
int currentPage = currentRowIndex / iter.getRangeSize();
currentPage++;
_logger.info("Current Page:" + currentPage);
int indexOnPage = (currentRowIndex % iter.getRangeSize());
_logger.info("Current index on Page:" + indexOnPage);
// get an ADF attributevalue from the ADF page definitions
AttributeBinding attr = (AttributeBinding) bindingContainer.getControlBinding("selectedRow1");
StringBuffer sb = new StringBuffer();
sb.append("row index overall: ");
sb.append(currentRowIndex);
sb.append(" row index on page: ");
sb.append(indexOnPage);
sb.append(" Page: ");
sb.append(currentPage);
attr.setInputValue(sb.toString());
}

To get the index of the selected row in the whole rowset we need the iterator and get the RowSetIterator from it. The rowSetIterator method getCurrentRowIndex() returns the index of the current row (line 5). The current page is calculated by dividing the current index through the range size (line 7). The final information is the index of the selected row on the page which is calculated as the current index modulo the range size (line 10). The rest of the listener build a string out of this information and writes it to a pageDef variable which is referenced in an outputfield on the page.

<af:outputText value="#{bindings.selectedRow1.inputValue}" id="ot8" partialTriggers="b2"/>

Here are some images from the sample application.

The sample application is build using JDev 12.1.3 and uses the HR DB schema. The sample can be downloaded from  Github