Wednesday, May 6, 2009

How to re-render selected datarows?

Some requirements I had during my course of attachment was to have a
1. DataTable with a checkbox for each data row.
2. Scrollable DataTable for (1).
3. Re-render a table with the selected(via checkbox) data rows once a button(e.g. "Show Selection") is clicked.
4. Have paging mechanism for (3).

For (2), extendedDataTable was used with richfaces 3.3.1 beta 5 for the scrolling effect. As for (4), it was achieved using a rich:dataTable embedded in a rich:panel, initially I wanted to use a modalPanel but the paging mechanism was faulty so rich:panel was used instead.

I did a search using google for the method to re-render a panel with selected datarows for 2 whole weeks..to my surprise, nothing similar was found o.O'' The main problem I had was actually about re-rendering the selected datarows in the same page, things would have been much simpler if the user requirements were to just select the checkboxes and press the submit button.

My humble workaround as follows:
Requirements:
- Firefox plug-in: Firebug
- Basic knowledge of javascript
- Basic understanding of converter, decode and validate for UIInput object.

Part I: In the JSF file -
1. Using extendedDataTable for the scrolling data table requirement
2. Using 'dummy' boolean variable, selected in product.java
3. Using h:selectBooleanCheckBox for checkbox in each datarow
4. A hidden variable for each datarow called rowNum whose value is the unique id identifying each Product.
5. A hidden UIInput object, productArray which stores the array of unique ids selected.

<rich:extendedDataTable enableContextMenu="false" var="product" rowKeyVar="rowIndex" value="#{backingbean.dataModel}">

<rich:column id="select">
<f:facet>
<h:outputText value="Select"/>
<h:selectBooleanCheckBox id="chkBox" value="#{product.selected}"/>
<h:inputHidden value="#{product.id}" id="rowNum"
</f:facet>
</rich:column>

.........

<h:inputHidden id="productArray" binding="#{backingBean.hiddenSelectedId}" converter="#{backingBean.productArrayConverter}"/>


Let me explain the rationale for using enableContextMenu="false" for the extendedDataTable. This variable is set so that the dropdown menus for sorting are disabled, this is proved to be working for richfaces 3.3.1 beta5 release.

Another thing to note is, 'selected' must be a boolean variable and in this case, its a dummy variable with getter and setter but is not stored into the database.

We have one more hidden variable,productArray which stores the array of productIds that were selected. Binding to a UIInput Object(in the backingBean is required) as we are re-rendering the same page, instead of pressing the submit button.(elaborate later). A converter is required to convert the format of the list of values, we will touch on this later.

Part II: Using Javascript in JSF to retrieve the selected checkbox values
- How to include .JS file in JSF?
Javascript must always be placed within the head tags.
<head>
<script type="text/javascript" src="#{faceContext.externalContext.request.ContextPath}/js/product.js}"
</head>

- How to call the javascript function once "show selected" button is pressed?
An AJAX button is used as I wanted to re-render part of the page only.
Once the button is clicked, the javascript function - setProductArray is called and takes in the no. of rows that were displayed for selection. We need this information to loop through the checkboxes that were displayed.

<a4j:commandButton id="showSelected" value="Show Selected" reRender="SelectedProductPanel" action="#{backingBean.retrieveSelectedProducts}" ajaxSingle="true" onclick="setProductArray(#{backingBean.dataModel.rowCount})">

Afterwhich, it goes to the backing bean and calls retrieveSelectedProducts function.
To retrieve information of the datarows that were displayed then selected, we need the unique identifier for each product, which is the productId. This is also why we used productId as the value for the hidden variable, rowNum.


Part III: The task to achieve in the javascript function, setProductArray:
1. Loop through all the checkboxes displayed.
2. Check if a checkbox is selected
3. Then retrieving the value for the hidden variable for that particular selected row and storing it in the array.

NOTE: For element id, use the firebug console to view them when the page is loaded.
And most importantly, since javascript is used, subview/form/table names should not be altered else the javascript will not be able to find the element.

function setProductArray(length){
var arrayLength = -1;
var checkedProductId = new Array();

if(length != null){
arrayLength = parseInt(length);
}

if(arraylength > -1){
for(var i = 0; i < arrayLength; i++){
var checkBox = document.getElementById('formView:table:'+ i + 'checkBox');
if(checkBox.checked){
var productId = document.getElementById('formView:table:'+ i + 'rowNum').value;
var nextArrayIndex = checkedProductId.length;
checkedProductId [nextArrayIndex] = productId;
}
}
}

document.getElementById('formView:table:productArray').value = checkedProductId;
//set value of array into declared hidden variable, productArray
}

Part IV: Inside the backing bean
In this function, we make use of UIInput object decode( ) and validate( ) functions to set the selected values into the object. By calling validate, the values get converted into an arrayList through the getAsObject( ) function in the converter.

public void retrieveSelectedProducts( ){
hiddenSelectedId.decode(FacesContext.getCurrentInstance());
hiddenSelectedId.validate(FaceContex.getCurrentInstance());
ArrayList selectedProductList = new ArrayList;

ArrayList selectedIds = (ArrayList)hiddenSelectedId.getValue( );
//cast object into integer arraylist
if(selectedIds.size()>0){
for(int i=0; i < selectedIds.size() ; i++){
Product product = productService.getProductById(selectedIds.get(i));
selectedProductList.add(product);
}
}
selectedDataModel.setWrappedData(selectedProductList);
//pass selected products list into dataModel then display in the dataTable embedded in the rich:Panel tags.
}

Part V: Inside the converter
The getAsObject( ) method in the converter is called when we call validate( ) in the above step.
The converter takes in the values we set in using javascript @
document.getElementById('formView:table:productArray').value = checkedProductId;

The values come in the form of productIds separated by commas e.g. 200,300,400. In getAsObject( ) method, we split and store them as separate elements in an array.

public Object getAsObject(FacesContext context, UIComponent component, String value) throws ConverterException{
ArrayList selectedIdsArr = new ArrayList();
if(!("").equalsIgnoreCase(value)){
String[] idList = value.split(",");

if(idList.length>0){
//loop through whole string array
for( int i = 0 ; i < idList.length ; i++){
Integer productId = Integer.parseInt)idList[i]);
selectedIdsArr.add(productId);
}
}
}
return selectedIdsArr;
}

To ensure that the format of productIds stored in ProductArray is consistent, in the getAsString( ) method, we do some basic formatting to make sure that the productIds are stored in xx,xx,xx format.

public String getAsString(FacesContext context, UIComponent component, Object value) throws ConverterException{
if(value instanceOf ArrayList){
String newVal = value.toString(); //values come in as [200], [300], [400]
newVal = newVal.replace(" ",""); //remove spacing and all formatting
newVal = newVal.replace("[",""); //to become 200,300,400
newVal = newVal.replace("]","");

return newVal;
}
throw new ConverterException("Not an instance of ArrayList");

}

Tuesday, May 5, 2009

General tips/ FAQ on JBOSS/AJAX components

Q: What does immediate="true" stands for?
=> When you use immediate=true, you bypass validation for e.g. mandatory fields.

Q: How do I rerender certain components/tables on a page?
=> Use AJAX reRender function. When calling reRender in AJAX components, make sure you enter a panel name and not a table name or anything else. In the example below, to rerender 2 panels use a ','.

Example:

<a4j: action="#{backingbean.retrieveSelectedData}" value="Show selected rows" rerender="selectedRowsPanel, allSelectionPanel">

<h:panelgroup id="selectedRowsPanel">
.....
</h:panelgroup>

<h:panelgroup id="allSelectionPanel">
.....
</h:panelgroup>

Q: How do I call a method in my backing bean, pass in parameter then return data to a page?

<h:commandLink immediate= "true" action= "#{backingBean.findProductByCategory}" value="Get product info">
<f:setPropertyActionListener target="#{backingBean.category}" value="#{category}"/>
</h:commandLink>

Backing bean:
public class ProductBean {
private cat;

public setCategory(Category category){
this.cat = category;
}

public String findProductByCategory( ){
....
}
}

Q: When the page is loaded, IE shows "Javascript runtime error" but not in firefox. What's wrong?
=> This maybe due to extra tags for example, form within form:


<h:form id="productInfo">
......
<h:form id="productForm">
....
</h:form>
......
</h:form>

Q: How do I call something like an onload( ) function that will be called once the page is loaded?
=> Here's a get around method:

Inside JSF: To be placed before your form/other code.
<h:inputHidden value="#{backingBean.resetValue}"/>

Inside backing bean: A string is returned as this is a getter. The value returned is not important, what we want is to run the codes to reset all values(for whatever reason)

public String getResetValue( ){
this.product = null;
...
return "";
}

What's this blog about?

Basically, this blog consolidates my knowledge gained for a six months attachment and dabbling with JBoss and AJAX components.

In this blog, I will be talking about unorthodox ways to achieve certain tasks using JBoss rich faces and AJAX components. Well, customer requirements regarding user interfaces are always troublesome, aren't they?