Wednesday, February 29, 2012

ADF 11g : Even Fancier ! Multi Master Multi Detail and how to highlight related detail records

A few weeks ago I wrote about how to highlight related detail records. That solution worked very well, but turned out to be not as fancy as expected. I needed to be able to implement multiple selection in the master as well. That turned out to be not very simple but I managed to end up with having a multi-master / multi-detail implementation.

In this post I show you how I did that.

First I had to enable multiple selection on the master table (Countries). To achieve this I had to remove the selectionListener and the selectedRowKeys. I also set rowSelection="multiple". The JSF code for the table now is :

1:  <af:table rows="#{bindings.CountriesView2.rangeSize}"  
2: fetchSize="#{bindings.CountriesView2.rangeSize}"
3: emptyText="#{bindings.CountriesView2.viewable ? 'No data to display.' : 'Access Denied.'}"
4: var="row"
5: value="#{bindings.CountriesView2.collectionModel}"
6: rowBandingInterval="0"
7: rowSelection="multiple"
8: id="t1"
9: styleClass="AFStretchWidth">

After this change, the second table (Locations) is no longer refreshed. That makes sense because I removed the selectionListener from the Countries table. To solve this problem I decided to use a clientListener and a ServerListener on the Countries table. The table component supports a clientListener of type 'selection', that fires accordingly.

1:    <af:clientListener method="rowSelected" type="selection"/>            
2: <af:serverListener type="invokeMatcher"
3: method="#{pageFlowScope.HighLightBean.matchEmFromJavaScript}"/>

The corresponding javascript function queues an event to invoke a serverside Java method. Notice line 4 and 5 where I get a handle to the table component, and I pass this table component to the managed bean.
1:     <f:facet name="metaContainer">  
2: <af:resource type="javascript">
3: function rowSelected(evt) {
4: var table = evt.getSource();
5: AdfCustomEvent.queue(table, "invokeMatcher", {}, true);
6: evt.cancel();
7: }
8: </af:resource>
9: </f:facet>

In the managed bean I have e method called "matchEmFromJavaScript". The code for this method is below. After doing some initialization, I get a handle to the table (line 5) and for each and every selected row (line 9) I get the rowdata and pass the corresponding row to the matchEm() method (line 12). This matchEm()method is almost the same as the one that I used in my previous post. There are however some minor changes because I have a Multi Select master now. The most important change is that I had to move code one level up because this method is now being called in a loop.
Otherwise only the last match is finally added to the selected rowkeys instead of all matching rows.

1:   public void matchEmFromJavaScript(ClientEvent ce) {  
2: // init
3: newSelection.clear();
4: String country = "";
5: RichTable countryTable = (RichTable)ce.getComponent();
6: // process selected rowkeys
7: RowKeySet rowKeySet = (RowKeySet)countryTable.getSelectedRowKeys();
8: CollectionModel cm = (CollectionModel)countryTable.getValue();
9: for (Object countryRowKey : rowKeySet) {
10: cm.setRowKey(countryRowKey);
11: JUCtrlHierNodeBinding rowData = (JUCtrlHierNodeBinding)cm.getRowData();
12: matchEm(rowData.getRow());
13: }
14: locTable.setSelectedRowKeys(newSelection);
15: AdfFacesContext.getCurrentInstance().addPartialTarget(locTable);
16: AdfFacesContext.getCurrentInstance().addPartialTarget(countryTable);
17: }

Now the MultiSelect Master and MultiSelect Detail are ready for use. However, as you can see below, the result is not yet satisfactory. I do have mutliple master records and multiple detail records selected, but I am not able to see how they are related.

So here is the question : Is there a way to add colors to the rows so it becomes clear how they are related ?

The answer is yes, however there are some tricky steps here.

First this requires a custom skin. In this skin I can define colors so that the the selected rows in the locations table one to have the same color as their selected parent row. I decided to create 5 predefined styles, style-0 ---> style-4, staring in lines 13, 21, 29, 37 and 45. What is very important here is the use of !important, because whithout that predicate it doesn't seem to work.

1:   af|table::data-row:highlighted af|column::data-cell,  
2: af|table::data-row:highlighted af|column::banded-data-cell{
3: background-color: rgb(184,223,253) !important;
4: }
5: af|table::data-row:selected:focused af|column::data-cell,
6: af|table::data-row:selected:focused af|column::banded-data-cell,
7: af|table::data-row:selected:inactive af|column::data-cell,
8: af|table::data-row:selected:inactive af|column::banded-data-cell,
9: af|table::data-row:selected af|column::data-cell,
10: af|table::data-row:selected af|column::banded-data-cell{
11: background-color:transparent !important;
12: }
13: af|table::data-row:selected:focused af|,
14: af|table::data-row:selected:focused af|,
15: af|table::data-row:selected:inactive af|,
16: af|table::data-row:selected:inactive af|,
17: af|table::data-row:selected af|,
18: af|table::data-row:selected af|{
19: background-color:Aqua !important;
20: }
21: af|table::data-row:selected:focused af|,
22: af|table::data-row:selected:focused af|,
23: af|table::data-row:selected:inactive af|,
24: af|table::data-row:selected:inactive af|,
25: af|table::data-row:selected af|,
26: af|table::data-row:selected af|{
27: background-color:Fuchsia !important;
28: }
29: af|table::data-row:selected:focused af|,
30: af|table::data-row:selected:focused af|,
31: af|table::data-row:selected:inactive af|,
32: af|table::data-row:selected:inactive af|,
33: af|table::data-row:selected af|,
34: af|table::data-row:selected af|{
35: background-color:Lime !important;
36: }
37: af|table::data-row:selected:focused af|,
38: af|table::data-row:selected:focused af|,
39: af|table::data-row:selected:inactive af|,
40: af|table::data-row:selected:inactive af|,
41: af|table::data-row:selected af|,
42: af|table::data-row:selected af|{
43: background-color:Orange !important;
44: }
45: af|table::data-row:selected:focused af|,
46: af|table::data-row:selected:focused af|,
47: af|table::data-row:selected:inactive af|,
48: af|table::data-row:selected:inactive af|,
49: af|table::data-row:selected af|,
50: af|table::data-row:selected af|{
51: background-color:Yellow !important;
52: }

This skin defines the styles, but now I need to make sure that selected rows take on that style.

First I created a List with available and unAvailable styles in my backing bean. I found this to be necessary in order to retain the same color for a country over several selection events.
1:  .........................  
2: private ArrayList availableStyles = new ArrayList();
3: private ArrayList unavailableStyles = new ArrayList();
4: public HighLightBean() {
5: init();
6: }
7: public void init(){
8: availableStyles.add("style-0");
9: availableStyles.add("style-1");
10: availableStyles.add("style-2");
11: availableStyles.add("style-3");
12: availableStyles.add("style-4");
13: unavailableStyles.clear();
14: }

As you can see I add the 5 predefined styles to the List with available colors.
Next step is to set the a style whenever a row is selected. This also means moving that style form available to unavailable. I use two additional maps to keep track of selected countries, the selCoMap that holds the newly selected countries, and the oldSelCoMap to hold the previously selected ones.

In the matchEmFromJavaScript() method I can now do all processing that I need. For instance I check if the selected row was already selected. If not, I add it to the map with selected rows, and apply the first available style to it (lines 15-18). If it was already selected, I add it to the map with selected rows keeping its previously defined style (lines 21,22). Finally (lines 28,29) all entries that remained in the oldSelCoMap are no longer selected rows anymore, so I make the corresponding styles available for the next run.

1:   public void matchEmFromJavaScript(ClientEvent ce) {  
2: // initialize and store previously selected values
3: oldSelCoMap.putAll(selCoMap);
4: selCoMap.clear();
5: newSelection.clear();
6: String country = "";
7: RichTable countryTable = (RichTable)ce.getComponent();
8: // process selected rowkeys
9: RowKeySet rowKeySet = (RowKeySet)countryTable.getSelectedRowKeys();
10: CollectionModel cm = (CollectionModel)countryTable.getValue();
11: for (Object countryRowKey : rowKeySet) {
12: cm.setRowKey(countryRowKey);
13: JUCtrlHierNodeBinding rowData = (JUCtrlHierNodeBinding)cm.getRowData();
14: country = (String)rowData.getRow().getAttribute("CountryId");
15: if (!oldSelCoMap.containsKey(country)){
16: selCoMap.put(country, availableStyles.get(0));
17: unavailableStyles.add(availableStyles.get(0));
18: availableStyles.remove(0);
19: }
20: else {
21: selCoMap.put(country, oldSelCoMap.get(country));
22: oldSelCoMap.remove(country);
23: }
24: matchEm(rowData.getRow());
25: }
26: // all entries that remained in the oldSelCoMap are no longer selected rows anymore.
27: // make the corresponding styles available for the next run.
28: availableStyles.addAll(oldSelCoMap.values());
29: unavailableStyles.removeAll(oldSelCoMap.values());
30: // clear the previously selected values
31: oldSelCoMap.clear();
32: // System.out.println("check avialable " + availableStyles.toString());
33: // System.out.println("check unavialable " + unavailableStyles.toString());
34: // set the selectedRowKeys for the locations table
35: locTable.setSelectedRowKeys(newSelection);
36: // refresh both tables
37: AdfFacesContext.getCurrentInstance().addPartialTarget(locTable);
38: AdfFacesContext.getCurrentInstance().addPartialTarget(countryTable);
39: }

With this in place I 'only' need to tell the table components what style to apply to the rows. I used EL to achieve this. On all columns of the Countries table and the Locations table I added the following expression:
1:  styleClass="#{(empty pageFlowScope.HighLightBean.selCoMap[row.CountryId])  ? '' : pageFlowScope.HighLightBean.selCoMap[row.CountryId]}"  

This expression takes the styleClass from the selCoMap Map that I defined in the backing bean. If there is no match the expression evaluates to "" and else sets the defined styleClass.
I think the result is pretty amazing.

Code can be for this post can be downloaded here.

Friday, February 10, 2012

ADF 11g Quicky 4 : Where is my cursor ?

In my current project I encountered a usability issue. The users where unable to see where the cursor was located. It was flickering, but on a page with ,multiple input components it was very difficult to locate the field where the cursor was located. I decided to help them out and that proofed to be both very simple and in the end very helpful. I used skinning to give the active field a colored background.

First I defined a skin in my trinidad-config.xml

 <?xml version="1.0" encoding="windows-1252" ?> 
<skins xmlns="">

Next I made sure that the application uses the skin by defining it in the trinidad-config.xml
 <?xml version="1.0" encoding="windows-1252"?>
<trinidad-config xmlns="">

Finally I created the corresponding style sheet and added entries for focus pseudoclass.
af|inputText::content:focus {background-color:rgb(214,214,255);}
af|inputDate::content:focus {background-color:rgb(214,214,255);}
af|inputListOfValues::content:focus {background-color:rgb(214,214,255);}

The result is highlighted fields whenever the cursor is in it.
For an LOV field:

For a date field:

For an inputtext field:

Imagine the effect on a page with several dozens of inpt fields....

Monday, February 06, 2012

ADF 11g : Fancy Master Detail or how to Highlight Related Detail Records

Last week I a had a rather interesting question: Is it possible to highlight related data that is in different af:table components ? Sure you can, so I decided to write a simple example application, and share the knowledge in this post.

The use Case.
After selecting a row in one table I need to highlight related rows in another table.

The implementation.
To implement this, I create a page based on ADF Business Components for Countries and Locations, both from the HR schema.
Note that, in order to make these tables behave independently, I do not use a viewlink between countries and locations.
This means that there will be no master detail relationship and that the locations table will always show all locations, not only the ones in the selected country.

The way to go here is :
a) Select 1 country in the left table
b) In the selectionListener of that country table add each and every location that is in the selected country to the selectedRowKeys of the locationsTable.

The Challenges.
The First challenge is the creation of the multiselect table component containing the Locations. Now why would that be a challenge. I allready described in one of my previous posts where I ran into an issue with mutliselection: The developers guide (22.5.1) has the solution. DO NOT check ‘enable selection’ when you want to use multiple selection in a table. And I do need mutliselection here because I want to select all related records. So create the table without selectionListener and selectedRowKeys attributes.

Next step is the creation of a custom TableSelectionHandler. The Handler is needed, because when selecting a row in the countries table, I need to do more then just selecting that row; I also need to process the selected row and match it with available Locations. This custom selectionHandler is described by Frank Nimphius in his book. Code for the Listener is in the fragment below.
1:  public static void makeCurrent(SelectionEvent selectionEvent){  
2: RichTable _table = (RichTable) selectionEvent.getSource();
3: CollectionModel _tableModel = (CollectionModel) _table.getValue();
4: JUCtrlHierBinding _adfTableBinding = (JUCtrlHierBinding) _tableModel.getWrappedData();
5: DCIteratorBinding _tableIteratorBinding = _adfTableBinding.getDCIteratorBinding();
6: Object _selectedRowData = _table.getSelectedRowData();
7: JUCtrlHierNodeBinding _nodeBinding = (JUCtrlHierNodeBinding) _selectedRowData;
8: //get the row key from the node binding and set it as the current row in the iterator
9: Key _rwKey = _nodeBinding.getRowKey();
10: _tableIteratorBinding.setCurrentRowWithKey(_rwKey.toStringFormat(true));
11: }

I call this method in the selectionListener of the Countries table.
1:  <af:table rows="#{bindings.CountriesView2.rangeSize}"  
2: fetchSize="#{bindings.CountriesView2.rangeSize}"
3: emptyText="#{bindings.CountriesView2.viewable ? 'No data to display.' : 'Access Denied.'}"
4: var="row"
5: value="#{bindings.CountriesView2.collectionModel}"
6: rowBandingInterval="0"
7: selectedRowKeys="#{bindings.CountriesView2.collectionModel.selectedRow}"
8: selectionListener="#{pageFlowScope.HighLightBean.onCountryTableSelect}"
9: rowSelection="single" id="t1"
10: styleClass="AFStretchWidth>
11: <af:column ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,

The onCountryTableSelect method, not only calls the makeCurrent() method in my GenericTableSelectionHandler, but it also calls out to the matchEm method that will match the current Country, to all available Locations.
1:  public void onCountryTableSelect(SelectionEvent selectionEvent) {  
2: // your pre-trigger code goes here ...
3: GenericTableSelectionHandler.makeCurrent(selectionEvent);
4: //your post-trigger code goes here ...
5: DCIteratorBinding conIter = ADFUtils.findIterator("CountriesView2Iterator");
6: matchEm(conIter.getCurrentRow());
7: }

You might be tempted to use setSelectedRowKeys() on the LocationsIterator. This however doesn't work. You will need to use the table's value which is an instance of CollectionModel. This sounds complicated perhaps, but in fact it isn't.
1:  private void matchEm(Row r) {  
2: RowKeySetImpl newSelection = new RowKeySetImpl();
3: locTable.getSelectedRowKeys().clear();
4: String countryId = (String)r.getAttribute("CountryId");
5: //the CollectionModel provides access to the ADF Binding for this table
6: CollectionModel model = (CollectionModel)locTable.getValue();
7: int rowCount = model.getRowCount();
8: for (int i = 0; i &lt; rowCount; i++) {
9: model.setRowIndex(i);
10: Object rowkey = model.getRowKey();
11: JUCtrlHierNodeBinding rowdata = (JUCtrlHierNodeBinding)model.getRowData(i);
12: Row loc = rowdata.getRowAtRangeIndex(i);
13: if (loc.getAttribute("CountryId").toString().equalsIgnoreCase(countryId)) {
14: System.out.println("found a match for locations " + loc.getAttribute("City"));
15: System.out.println("adding key " + loc.getKey());
16: newSelection.add(rowkey);
17: }
18: }
19: locTable.setSelectedRowKeys(newSelection);
20: AdfFacesContext.getCurrentInstance().addPartialTarget(locTable);
21: }

Now what this code does is the following:
It gets all rows that are in the table's ColectionModel, and for each of these rows it gets the rowkey.
Whenever the CountryId in that row is the same as the selected country, the rowkey is added to the rowkeyset containing the new selection.
Finally the, rowkeyset is used to set the selectedRowKeys of the table, and the table is added to the partialtargets using following code:

The result.
When you run the application, you will see the result. Select a country in the left table, and all related locations are selected in the right table.

Change the country, and see how selected locations are also updated.

You can download the sample workspace here.
This post was originally published on the amis technology blog.