Thursday, May 2, 2013

ADF With no Bindings: Understanding the Trinidad CollectionModel

Did you ever wonder what the trinidad CollectionModel is good for? Well, I did.
According to the docs...
10.2 Displaying Data in Tables 

The table component uses a CollectionModel class to access the data in the underlying collection. This class extends the JSF DataModel class and adds on support for row keys and sorting. In the DataModel class, rows are identified entirely by index. This can cause problems when the underlying data changes from one request to the next, for example a user request to delete one row may delete a different row when another user adds a row. To work around this, the CollectionModel class is based on row keys instead of indexes. 

If we look at MyFaces Trinidad javadoc, it states almost the same.
There are only two points described above: Support for row keys aside from index; and to enable sorting.

But digging deeper by observing the behavior of tables based on ADFm and creating a trivial implementation, I noted the following purposes:
  • On demand data access
  • Support for row keys aside from index
  • To enable sorting
  • Filtering
  • Data caching

How to implement CollectionModel?

To implement a CollectionModel you need to provide implementation of at least the following abstract methods:
public Object getRowKey(); 
    public void setRowKey(Object object);
    public boolean isRowAvailable();
    public int getRowCount();
    public Object getRowData();
    public int getRowIndex();
    public void setRowIndex(int i);
    public Object getWrappedData();
    public void setWrappedData(Object object);

How does CollectionModel suppose to behave?

The collection model is a black box in which you will provide input and get output. There are two ways to provide an input:
  • setRowIndex(int i)
  • setRowKey(Object object)
The table component is doing the following on collection model upon initial load:
  1. Reset the model by calling setRowIndex(int i) with a parameter of -1 and calling setRowKey(Object object) with a null parameter; It does this at least twice.
  2. invokes int getRowCount() to have an estimate of the total number of records.
  3. call setRowIndex(int i) with index starting from 0.
  4. check if there is data on the current index by calling isRowAvailable(). If it returns true then it will proceed to step 5 below else stop from here.
  5. call getRowData(). to retrieve the data on the current index as set on step 3. Next.. back to step 3 until the browser window is full.
  6. If the user scrolls down then back to step 3.
What does this mean? It means that if you implement a CollectionModel you need to ensure that when the user calls one of the input methods above, you should be able to give a proper output upon their subsequent call to: isRowAvailable(), getRowData, getRowKey(), and getRowIndex().

Sample Trivial Implementation

package soadev.view.model;

import java.util.List;

import org.apache.myfaces.trinidad.model.CollectionModel;

import soadev.domain.Job;


public class MyCollectionModel extends CollectionModel {
    private List wrappedData;
    private int index;

    public MyCollectionModel() {

    }

    private int indexOf(Object rowKey) {
        int result = -1;
        for (Object obj : wrappedData) {
            result++;
            Job job = (Job)obj;
            if (rowKey.equals(job.getJobId())) {
                return result;
            }
        }
        return result;
    }

    public MyCollectionModel(List wrappedData) {
        this.wrappedData = wrappedData;
    }

    public Object getRowKey() {

        if (index < 0 || index >= wrappedData.size()) {
            return null;
        }
        Job job = (Job)wrappedData.get(index);
        System.out.println("getRowKey() return: " + job.getJobId());
        return job.getJobId();
    }

    public void setRowKey(Object object) {
        System.out.println("setRowKey(Object object) " + object);
        if (object == null) {
            index = -1;
        } else {
            index = indexOf(object);
        }
    }

    public boolean isRowAvailable() {
        System.out.println("isRowAvailable()  return: " +
                           (index > -1 && index < wrappedData.size()));
        return index > -1 && index < wrappedData.size();
    }

    public int getRowCount() {
        System.out.println("getRowCount() return: " + wrappedData.size());
        return wrappedData.size();
    }

    public Object getRowData() {
        System.out.println("getRowData()");
        if (isRowAvailable()) {
            System.out.println("return: " + wrappedData.get(index));
            return wrappedData.get(index);
        }
        return null;
    }

    public int getRowIndex() {
        System.out.println("getRowIndex() return: " + index);
        return index;
    }

    public void setRowIndex(int i) {
        System.out.println("setRowIndex(int i) " + i);
        index = i;
    }

    public Object getWrappedData() {
        System.out.println("getWrappedData() return: " + wrappedData);
        return wrappedData;
    }

    public void setWrappedData(Object object) {
        System.out.println("setWrappedData(Object object)" + object);
        this.wrappedData = (List)object;
    }

}
Sample console output showing invocation sequence
Target URL -- http://127.0.0.1:7101/Application1-Web-context-root/faces/basic_collection_model.jspx
setRowIndex(int i) -1
setRowKey(Object object) null
isRowAvailable()  return: false
setRowKey(Object object) null
isRowAvailable()  return: false
setRowKey(Object object) null
isRowAvailable()  return: false
setRowKey(Object object) null
isRowAvailable()  return: false
setRowIndex(int i) -1
getRowCount() return: 500
setRowKey(Object object) null
isRowAvailable()  return: false
setRowIndex(int i) 0
isRowAvailable()  return: true
getRowData()
isRowAvailable()  return: true
return: soadev.domain.Job@3a971ffdjob Title 0
getRowKey() return: job0
isRowAvailable()  return: true
getRowKey() return: job0
getRowIndex() return: 0
getRowIndex() return: 0
getRowIndex() return: 0
getRowIndex() return: 0
getRowIndex() return: 0
setRowIndex(int i) 1
isRowAvailable()  return: true
getRowData()
isRowAvailable()  return: true
return: soadev.domain.Job@1db884a1job Title 1
getRowKey() return: job1
isRowAvailable()  return: true
getRowKey() return: job1
getRowIndex() return: 1
getRowIndex() return: 1
getRowIndex() return: 1
getRowIndex() return: 1
getRowIndex() return: 1
setRowIndex(int i) 2
...
... and so on

GitHub

5 comments:

  1. Great post
    Helped me to understand Collection Model.

    ReplyDelete
  2. The implementation of getRowCount() may be a costly operation if the data is quite large and is to be retrieved from a Database and SQL takes a long time. So how can we overcome this?

    ReplyDelete
  3. Hii Neerav.. I have the same doubt over here.. can anybody answer to this question please...
    What is the best step to come out of this Problem..

    ReplyDelete
  4. Thanks For Sharing This Great Post Dude
    Plots in Hoskote

    ReplyDelete