Sorting and paginating in the database with Richfaces

Richfaces is designed to easily add ajax capabilities to a Java Server Faces (JSF) application, without the developer having to write any javascript.  All Richfaces components are skinnable, and when a given skin is applied, all the components on a given website have the same look and feel.

Unfortunately, out of the box, Richfaces does not provide a way to sort and paginate results using the database–all results must be passed to Richfaces, which then sorts and paginates in memory.  This might prove to be showstopper if you have a large result set.

When a Richfaces component does not do what you need by default, you have several options.

  1. Use another component library to provide the missing component
  2. Use simple Richfaces components, such as Rich Panel, to provide a canvas that has the Richfaces look and feel, and then on that canvas, use regular (non-ajax) links.
  3. Extend the capability of one or more Richfaces components to provide the functionality you need.

The Seam example “Seamdiscs” takes the first approach.  It uses a Trinidad table component that natively supports paging and sorting via the database, and places this component in the middle of a site that uses Richfaces for the remainder of the components.  Of note, if you run the seamdiscs example, links at the bottom of the main page describe exactly what changes you need to make to your application to get both Trinidad and Richfaces working together smoothly.  Here is picture of what this approach looks like:

Richfaces and Trinidad components together

As you can see, the Trinidad table doesn’t quite match the look and feel of the rest of the site. Consequently, you may want to examine one of the other approaches. What I will explain here is the third approach.  The Richfaces documentation states that if you want to roll your own data model, to support such features as sorting and paginating in the database, or perhaps to pull from a non-standard data source, you should extend the org.ajax4jf.model.ExtendedDataModel.  Further, the documentation suggests inheriting in particular from the SerializableDataModel, which itself inherits from ExtendedDataModel.  The first time you create a concrete class that inherits from SerializableDataModel, you may be overwhelmed at the number of abstract methods that you have to implement.  But, don’t worry–those methods will all be explained here.

public Object getRowKey() and public void setRowKey(Object key)

These methods should just get and set an instance variable that is Serializable, and which you can use to look up an individual object that will populate a row in your table.  In my case, each row of my table will correspond to a Folder object, and I look up Folder objects in the database via a pk that is an Integer, so my instance variable is an Integer:

private Integer currentPk;
@Override
public Object getRowKey()
{
    return currentPk;
}

@Override
public void setRowKey(Object key)
{
    this.currentPk = (Integer) key;
}

The most important method is the walk method.  This method will be called by Richfaces, and will give you an opportunity to pull from the database just those records that will be shown to the user.  However, it is important to note that this method can potentially be called many times during one request, and we only want to make one call to the database.  So, we want to cache the results, but we also don’t want to cache them too aggressively, for if a person requests a different page of results, or changes the sort order, then we want to fetch new results.  So, two other methods that you must override, public void update() and public SerializableDataModel getSerializableModel(Range range) come into play as well.  Here is the code, discussion follows:


private boolean detached = false;
private List wrappedKeys = null;
private final Map wrappedData = new HashMap();
private String sortField = "lastname";
@In
private IFormsService serviceForms;

@Override
public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) throws IOException
{
    if (detached && getSortFieldObject() != null)
    {
        for (Integer key : wrappedKeys)
        {
            setRowKey(key);
            visitor.process(context, key, argument);
        }
    } else
    {
        int firstRow = ((SequenceRange) range).getFirstRow();
        int numberOfRows = ((SequenceRange) range).getRows();
        wrappedKeys = new ArrayList();
        for (FolderDTO folder : serviceForms.findFolders(firstRow, numberOfRows, sortField, descending)
        {
            wrappedKeys.add(folder.getPk());
            wrappedData.put(folder.getPk(), folder);
            visitor.process(context, folder.getPk(), argument);
        }
    }
}

@Override
public SerializableDataModel getSerializableModel(Range range)
{
    if (wrappedKeys != null)
    {
        detached = true;
        return this;
    }
    return null;
}


@Override
public void update()
{
    if (getSortFieldObject() != null)
    {
        String newSortField = getSortFieldObject().toString();
        if (newSortField.equals(sortField))
        {
            descending = !descending;
        }
        sortField = newSortField;
    }
    detached = false;
}

We have two checks to see if we should return cached data, or if we should fetch new data.  The first boolean, detached, starts as false.  It will be set to “true” when SerializableDataModel getSerializableModel(Range range) is called, and back to false when public void update() is called.  In this manner, the model will not be updated when the jsf component is rebuilt on postback, but rather when the new model is being built.  The other check, for getSortFieldObject, is in place for the following reason:  When it is not the table itself which has an action taken on it, but rather something outside the table (such as a DataScroller), the update method will not be called.  Since we will be passing our own parameter (the sort field) whenever a column header is clicked on, we know that getSortFieldObject() will be null if it is something outside the table that is clicked.  Here is the getSortFieldObject() method:


private Object getSortFieldObject()
{
    FacesContext context = FacesContext.getCurrentInstance();
    Object sortFieldObject =  context.getExternalContext().getRequestParameterMap().get("sortField");
    return sortFieldObject;
}

Continuing with methods we must override:  public int getRowCount():

private Integer rowCount;
@Override
public int getRowCount()
{
    if (rowCount == null)
    {
        rowCount = serviceForms.countFolders();
    }
    return rowCount;
}

Here, we are just returning the total number of folders.

The next method is public Object getRowData():

@Override
public Object getRowData()
{
    if (currentPk == null)
    {
        return null;
    } else
    {
        FolderDTO folder = wrappedData.get(currentPk);
        if (folder == null)
        {
            folder = serviceForms.getFolder(currentPk);
            wrappedData.put(currentPk, folder);
        }
        return folder;
    }
}

This method uses the currentPk, which is passed in via the setRowKey method, to look up an object.  Since the walk method will have been called first, all objects should be in wrappedKeys.  The lookup via the dao is primarily a failsafe.

public boolean isRowAvailable().

@Override
public boolean isRowAvailable()
{
    if (currentPk == null)
    {
        return false;
    }
    if (wrappedKeys.contains(currentPk))
    {
        return true;


    }
    if (wrappedData.entrySet().contains(currentPk))
    {
        return true;
    }
    {
    if (serviceForms.getFolder(currentPk) != null)
    {
        return true;
    }
    return false;
}

This method just does what its name suggests..it returns if a given pk (set via the setRowKey method) actually corresponds to an object.

public void setRowIndex(int rowIndex), public void setWrappedData(Object data), and public Object getWrappedData() are all unused, so you can simply throw an UnsupportedOperationException():

@Override
public int getRowIndex()
{
    throw new UnsupportedOperationException();
}

@Override
public Object getWrappedData()
{
    throw new UnsupportedOperationException();
}

@Override
public void setRowIndex(int rowIndex)
{
    throw new UnsupportedOperationException();
}

Now that you have implemented all the methods, there’s one more thing to note: this class should be either Session or Conversation scoped. Among other things, this ensures that the previous sort order will be preserved, so that we know if we should reverse it.

It’s time for the xhtml. We start with an ordinary Rich DataTable.  However, we add in an actionparam with each header.  This actionparam will be sent back to our table, and will let us know which header the person clicked on.  This information allows us to determine by what column we should sort the table.



    
        
            
                
            
        
        
    
    
        
            
                
            
        
        
        
    

To this, we add a datascroller, and we’re done:


Update: See the next post for instructions on abstracting the described class into a superclass and a subclass.

Advertisements

25 thoughts on “Sorting and paginating in the database with Richfaces

  1. Thanks for the write up!

    Did you try to use the rich:dataTable’s sortBy functionality?
    I wonder if it is possible to tie that into your code?

    Many thanks,
    Micke

  2. David,

    I could use some more help.

    Comment: setWrappedData() is unused as per your post, but not in your code.

    Is FolderDTO just an Entity class ?
    In the walk method, parameter descending is unresolved after compilation.
    IFormsService is a service component that implements findFolders – do you mind posting the code for that. Is it a Spring component wired via XML ?

    Regards
    Franco

  3. Congratulations for your article!
    I have had to implement getRowIndex and setRowIndex to avoid UnsupportedOperationException. I have just done it this way:

    `
    private int rowIndex;
    @Override
    public void setRowIndex(int index) {
    //throw new UnsupportedOperationException();
    this.rowIndex = index;
    }

    @Override
    public int getRowIndex() {
    //throw new UnsupportedOperationException();
    return this.rowIndex;
    }
    `

    It works ok for me and true pagination is accomplished fetching only the needed data.
    On the other hand, Mikael I have tried to use this with rich:column and their sortBy, doing that a -1 is passed to numberOfRows and it ends in the following error:

    `
    java.lang.IllegalArgumentException: Negative (-1) parameter passed in to setMaxResults
    at org.hibernate.ejb.QueryImpl.setMaxResults(QueryImpl.java:106)
    at es.rbcdexia.risk.online.action.BenchmarkList.getResultList(BenchmarkList.java:112)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.jboss.seam.util.Reflections.invoke(Reflections.java:21)
    at org.jboss.seam.intercept.RootInvocationContext.proceed(RootInvocationContext.java:31)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:56)
    at org.jboss.seam.transaction.RollbackInterceptor.aroundInvoke(RollbackInterceptor.java:31)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:68)
    at org.jboss.seam.core.BijectionInterceptor.aroundInvoke(BijectionInterceptor.java:46)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:68)
    at org.jboss.seam.transaction.TransactionInterceptor$1.work(TransactionInterceptor.java:38)
    at org.jboss.seam.util.Work.workInTransaction(Work.java:41)
    at org.jboss.seam.transaction.TransactionInterceptor.aroundInvoke(TransactionInterceptor.java:32)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:68)
    at org.jboss.seam.core.MethodContextInterceptor.aroundInvoke(MethodContextInterceptor.java:42)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:68)
    at org.jboss.seam.intercept.RootInterceptor.invoke(RootInterceptor.java:107)
    at org.jboss.seam.intercept.JavaBeanInterceptor.interceptInvocation(JavaBeanInterceptor.java:166)
    at org.jboss.seam.intercept.JavaBeanInterceptor.invoke(JavaBeanInterceptor.java:102)
    at es.rbcdexia.risk.online.action.BenchmarkList_$$_javassist_7.getResultList(BenchmarkList_$$_javassist_7.java)
    at es.rbcdexia.risk.online.util.BenchExtDataModel.walk(BenchExtDataModel.java:125)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.jboss.seam.util.Reflections.invoke(Reflections.java:21)
    at org.jboss.seam.intercept.RootInvocationContext.proceed(RootInvocationContext.java:31)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:56)
    at org.jboss.seam.transaction.RollbackInterceptor.aroundInvoke(RollbackInterceptor.java:31)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:68)
    at org.jboss.seam.core.BijectionInterceptor.aroundInvoke(BijectionInterceptor.java:46)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:68)
    at org.jboss.seam.core.MethodContextInterceptor.aroundInvoke(MethodContextInterceptor.java:42)
    at org.jboss.seam.intercept.SeamInvocationContext.proceed(SeamInvocationContext.java:68)
    at org.jboss.seam.intercept.RootInterceptor.invoke(RootInterceptor.java:107)
    at org.jboss.seam.intercept.JavaBeanInterceptor.interceptInvocation(JavaBeanInterceptor.java:166)
    at org.jboss.seam.intercept.JavaBeanInterceptor.invoke(JavaBeanInterceptor.java:102)
    at es.rbcdexia.risk.online.util.BenchExtDataModel_$$_javassist_15.walk(BenchExtDataModel_$$_javassist_15.java)
    at org.richfaces.model.ModifiableModel.prepareCollection(ModifiableModel.java:142)
    at org.richfaces.model.ModifiableModel.walk(ModifiableModel.java:111)
    at org.ajax4jsf.component.UIDataAdaptor.walk(UIDataAdaptor.java:1127)
    at org.richfaces.renderkit.AbstractRowsRenderer.encodeRows(AbstractRowsRenderer.java:106)
    at org.richfaces.renderkit.AbstractRowsRenderer.encodeRows(AbstractRowsRenderer.java:91)
    at org.richfaces.renderkit.AbstractRowsRenderer.encodeChildren(AbstractRowsRenderer.java:138)
    at javax.faces.component.UIComponentBase.encodeChildren(UIComponentBase.java:812)
    at org.ajax4jsf.renderkit.RendererBase.renderChild(RendererBase.java:282)
    at org.ajax4jsf.renderkit.AjaxChildrenRenderer.encodeAjaxComponent(AjaxChildrenRenderer.java:125)
    at org.ajax4jsf.renderkit.AjaxChildrenRenderer.encodeAjaxChildren(AjaxChildrenRenderer.java:68)
    at org.ajax4jsf.renderkit.AjaxChildrenRenderer.encodeAjaxComponent(AjaxChildrenRenderer.java:116)
    at org.ajax4jsf.renderkit.AjaxChildrenRenderer.encodeAjaxChildren(AjaxChildrenRenderer.java:68)
    at org.ajax4jsf.renderkit.AjaxChildrenRenderer.encodeAjaxComponent(AjaxChildrenRenderer.java:116)
    at org.ajax4jsf.renderkit.AjaxChildrenRenderer.encodeAjaxChildren(AjaxChildrenRenderer.java:68)
    at org.ajax4jsf.renderkit.AjaxChildrenRenderer.encodeAjaxComponent(AjaxChildrenRenderer.java:116)
    at org.ajax4jsf.renderkit.AjaxContainerRenderer.encodeAjax(AjaxContainerRenderer.java:123)
    at org.ajax4jsf.component.AjaxViewRoot.encodeAjax(AjaxViewRoot.java:673)
    at org.ajax4jsf.component.AjaxViewRoot.encodeChildren(AjaxViewRoot.java:544)
    at javax.faces.component.UIComponent.encodeAll(UIComponent.java:886)
    at com.sun.facelets.FaceletViewHandler.renderView(FaceletViewHandler.java:592)
    at org.ajax4jsf.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:108)
    at org.ajax4jsf.application.AjaxViewHandler.renderView(AjaxViewHandler.java:189)
    at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:106)
    at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:251)
    at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:144)
    at javax.faces.webapp.FacesServlet.service(FacesServlet.java:245)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:83)
    at org.jboss.seam.web.MultipartFilter.doFilter(MultipartFilter.java:85)
    at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
    at org.jboss.seam.web.ExceptionFilter.doFilter(ExceptionFilter.java:64)
    at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
    at org.jboss.seam.web.RedirectFilter.doFilter(RedirectFilter.java:45)
    at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
    at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:73)
    at org.ajax4jsf.webapp.BaseXMLFilter.doXmlFilter(BaseXMLFilter.java:154)
    at org.ajax4jsf.webapp.BaseFilter.handleRequest(BaseFilter.java:260)
    at org.ajax4jsf.webapp.BaseFilter.processUploadsAndHandleRequest(BaseFilter.java:366)
    at org.ajax4jsf.webapp.BaseFilter.doFilter(BaseFilter.java:493)
    at org.jboss.seam.web.Ajax4jsfFilter.doFilter(Ajax4jsfFilter.java:60)
    at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
    at org.jboss.seam.web.LoggingFilter.doFilter(LoggingFilter.java:58)
    at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
    at org.jboss.seam.servlet.SeamFilter.doFilter(SeamFilter.java:158)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
    at org.jboss.web.tomcat.se
    13:11:20,640 ERROR [STDERR] curity.SecurityAssociationValve.invoke(SecurityAssociationValve.java:179)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:432)
    at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
    at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:262)
    at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
    at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
    at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:446)
    at java.lang.Thread.run(Thread.java:619)

    `

    The code where I assign the numberOfRows is this:

    `
    @SuppressWarnings(“unchecked”)
    public List getResultList(int firstRow, int numberOfRows) {
    List lista = entityManager.createQuery(this.getEjbql()).setFirstResult(firstRow).setMaxResults(numberOfRows).getResultList();
    return lista;
    }
    `

    Any idea to solve this? I´ve tried to control this in the walk method assigning a constant, the number of rows I want, when a negative number is received but it does not work properly.
    any idea? thanks in advance!

  4. Thanks for all the comments. I hereby award the three of you charter commentator status 🙂

    @Mikael: I don’t use the sortBy functionality that they provide, but if you click on the column header in my example, you still get the column sorted, as that causes the getSortFieldObject method to return the name of the column that was clicked on.

    @Franco: Yes, FolderDTO is just an Entity class. The descending parameter is a boolean that I declare in the class, but I didn’t put it in the example. I declare it as
    private boolean descending.

    IFormsServices is an interface that has a findFolders method. In this case, I’m just using Seam to inject a class with a @Name of serviceForms, which implements that interface, via

    @In
    private IFormsService serviceForms;

    The serviceForms class is hundreds of lines long, but the relevant part is simply the creation of a javax.persistence.Query object, and then calling setFirstRow and setMaxRows on that object.

    @Jamie As I mentioned to Mikael, I’m not using the sortBy attribute provided by richFaces, but I’m still able to sort by clicking on a column header, due to the combination of the code in the column in the xhtml, and the getSortFieldObject method. Does that solve your issue?

  5. David,

    We should get a room 🙂

    Thanks, with your help and some tips from Jaime, I got the pagination to work.
    I can’t connect the dots between using query in EntityQuery and the extended data model walk().

    I have posted a reply on the Seam forum, so hoping Jaime can help.

    My problem is I can’t the column sorting to work with your example. I do agree with you that it solves what Jaime is trying to do with the sortBy.

    Once again, many thanks for writing this post.

    Franco

  6. David,

    Fear not, I meant we ALL find a room to hash this out.

    I’m too scared to post on the Seam forum – I might get banned.
    Please read my last post there.
    http://www.seamframework.org/Community/TroublesToUseDatascrollerAndExtendedDataModel

    Right now, it looks like I have it all working except for sorting via header clicks. After some debugging – update() is called which switches descending value to true, but by the time it gets to walk() it is back to default. I checked to make sure I do not have a local var of the same name. Do I need to put my extended model in a conversation scope ? Any tips ?

    Thanks
    Franco

  7. Yes, I actually have my SerializableDataModel scoped to the session, and then when someone clicks on one of the rows, I start a conversation when I take them to the edit page for that row.

  8. Thanks for writing this. This is exactly the problem I’m trying to solve.

    One question in your implementation. Adapting the code for my entities it’s rendering the table just fine and (yeehaw!) syncing the Richfaces pagination with the actual “chunks” coming from the database.

    However one problem I’m having is getting column sorting to work. I’m instrumented the code and discover that using the xhtml code in your example the update() method is called and I am correctly discovering which field should be the new sort order.

    But… nothing happens. Further instrumentation indicates that when the walk() method is called the original, default sort order is used. If I had to guess I’d say that somewhere an entirely new DataModel instance had been created. What do you think? Did you do anything special to keep your DataModel in scope?

    Thanks for any help.
    Greg

    My DataModel class:

    @Name(“testDataModel”)
    public class TestDataModel extends SerializableDataModel {

    private Integer currentPk;
    private boolean detached = false;
    private List wrappedKeys = null;
    private final Map wrappedData = new HashMap();
    private String sortField = “name”; // rational default
    private Integer rowCount;
    boolean descending = true;
    private int rowIndex;

    @In(create=true)
    private ClientHome clientHome;
    @In(create=true)
    private ClientList clientList;

    @Override
    public Object getRowKey() {
    return currentPk;
    }

    @Override
    public void setRowKey(Object key) {
    this.currentPk = (Integer) key;
    }

    @Override
    public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) throws IOException {
    if (detached && getSortFieldObject() != null) {
    for (Integer key : wrappedKeys) {
    setRowKey(key);
    visitor.process(context, key, argument);
    }
    } else {
    int firstRow = ((SequenceRange) range).getFirstRow();
    int numberOfRows = ((SequenceRange) range).getRows();
    wrappedKeys = new ArrayList();
    clientList.setMaxResults(numberOfRows);
    clientList.setFirstResult(firstRow);
    String sort = sortField;
    if( descending )
    sort = sort+” desc”;
    else
    sort = sort+” asc”;
    System.out.println(“WALK CALLED W/SORTFIELD: ” + sort);
    clientList.setOrder(sort);
    // TODO: Implement descending/ascending sort order
    // for (Client client : serviceForms.findFolders(firstRow, numberOfRows, sortField, descending) {
    for( Client client : clientList.getResultList()) {
    System.out.println(“Result: ” + client.getName());
    wrappedKeys.add(client.getIdClient());
    wrappedData.put(client.getIdClient(), client);
    visitor.process(context, client.getIdClient(), argument);
    }
    System.out.println(“——————-“);
    }
    }

    @Override
    public SerializableDataModel getSerializableModel(Range range) {
    if (wrappedKeys != null) {
    detached = true;
    return this;
    }
    return null;
    }

    @Override
    public void update() {
    System.out.println(“UPDATE CALLED! — “+getSortFieldObject());
    if (getSortFieldObject() != null) {
    String newSortField = getSortFieldObject().toString();
    if (newSortField.equals(sortField)) {
    descending = !descending;
    }
    sortField = newSortField;
    }
    //sortField = “adminContactName asc”;
    detached = false;
    System.out.println(“Update Sortfield: ” + sortField);
    }

    private Object getSortFieldObject() {
    FacesContext context = FacesContext.getCurrentInstance();
    Object sortFieldObject = context.getExternalContext().getRequestParameterMap().get(“order”);
    return sortFieldObject;
    }

    @Override
    public int getRowCount() {
    if (rowCount == null) {
    rowCount = clientList.getResultCount().intValue();
    }
    return rowCount;
    }

    @Override
    public Object getRowData() {
    if (currentPk == null) {
    return null;
    } else {
    Client client = wrappedData.get(currentPk);
    if (client == null) {
    clientHome.setClientIdClient(currentPk);
    client = clientHome.find();
    wrappedData.put(currentPk, client);
    }
    return client;
    }
    }

    @Override
    public boolean isRowAvailable() {
    if (currentPk == null) {
    return false;
    }
    if (wrappedKeys.contains(currentPk)) {
    return true;
    }
    if (wrappedData.entrySet().contains(currentPk)) {
    return true;
    }
    clientHome.setClientIdClient(currentPk);
    Client finding = clientHome.find();
    if (finding != null) {
    return true;
    }
    return false;
    }

    @Override
    public void setRowIndex(int index) {
    this.rowIndex = index;
    }

    @Override
    public int getRowIndex() {
    return this.rowIndex;
    }

    @Override
    public Object getWrappedData() {
    return wrappedData;
    }

    @Override
    public void setWrappedData(java.lang.Object data) {
    //this.wrappedData = data;
    }

  9. @Greg: Yes, I keep the data model in Session scope, and use Conversation scope when someone clicks on one of the items to edit it.

  10. Hi David,

    Can you share a bit of code how you set up the scoping (how classes were annotated in Seam)? I’ve been trying to put my DataModel in session scope but am getting errors. (Set up TestDataModel/TestDataModelBean, etc.) What did you put in the session, the model or your DTO? I ask because your sample xhtml code just uses #{newhireDataModel} not some kind of getInstance() method.

    I really appreciate this post. I hope RichFaces fixes this behavior in a future release.

    Greg

  11. After some holidays it´s nice to see that this interesting issue has been very active!
    @David The reason why I wanted to manage to make this compatible with RichFaces sortBy is this: Sometimes I am not just displaying a value stored in a bean property, but a real time processed data. For instance, some of the values are descriptions that should be displayed in the proper language which the user has already set. To make things more difficult the translations are in a database. It would be long to explain, but believe me that in my application it is better to store these values in a database rather than save them in a properties file.
    So a tipical column in this case is like this:

    <rich:column >
      <f:facet name="header">#{messages['CfgBenchmarkType.benchmarkTypeDes']}
      </f:facet>
      <h:outputText value="#{cfgDescriptionList.traducir(benchmark.cfgBenchmarkType.mulCode)}"/></rich:column>
    

    Pay attention to {cfgDescriptionList.traducir(benchmark.cfgBenchmarkType.mulCode)}
    I´m displaying what traducir method returns for the value of a property in a backing bean. There I access to a database according to some criteria.

    Using sortBy would be this simple:

    <rich:column sortBy="#{cfgDescriptionList.traducir(benchmark.cfgBenchmarkType.mulCode)}">
           <f:facet name="header">#{messages['CfgBenchmarkType.benchmarkTypeDes']} SORTBY
           </f:facet>
               <h:outputText value="#{cfgDescriptionList.traducir(benchmark.cfgBenchmarkType.mulCode)}"/>
    </rich:column>
    

    and with this RichFaces takes care of ordering that column values according to the EL expression that implies invoking traducir method without having to code SQL regarding this translation issue.

    @Franco I´ll have a look to your problem and will try to help you

  12. Thanks Jaime

    Good to know you got your batteries recharged. I go on vacation in a week.

    I really had to give up on this. My problem is that I am far too raw to even Seam/Hibernate so troubleshooting is no fun.

    Following David’s tip, I put my datamodel in session scope and voila – sorting using his technique worked.

    But then, I could never get the datascroller to reset when a new search is fired. The query would show the new results, but the datascroller would show with the same number of pages as the previous result.

    I tried various things and finally decided to just move on.

    Regards
    Franco

  13. David, I have two major problems. Any help is greatly appreciated.

    I always get -1 for numberOfRows in the following expression:

    int numberOfRows = ((SequenceRange) range).getRows();
    

    Methods getSerializableModel() and update() are also *never* reached.

    Thanks in advance.

  14. Hi everyone,
    Just for everybody to know that my project do call getWrappedData().
    RichFaces 3.2.2.GA
    Just in case 🙂

    P.S, if anybody has a clue what it should return please let me know

  15. Thanks for the article. It is very helpful.
    It would be great if i get help on my issue. If rowCount is 0, the request is not being fulfilled. i meant, the request is not going to walk method. Is there anything else i have to do?

  16. Thank you very much for the post, it is really explained well!

    Mohsen,

    i have the exact same problem as you have. I had to force its value to a constant to have some data fetched but I fear that it causes some other parts not to work anymore.

    I also have another problem, even though the datascroller is correctly linked to the dataTable (through in internal facet or externally linking with the “for” attribute), the dataScroller is always greyed out, as if there were not enough data returned to display it (even though that is certainly the case…)

    By the way, I am using Richfaces v3.3.1.

    any idea?

  17. Okay, after having checked the source code, it appears that:
    – the system, when generating the dataTable and the datascroller always go through the “modify” method of the “ModifiableModel” class. This class is the one responsible for calling the “walk” method of our coded dataModel. Fact is that, the “range” parameter passed to the walk method is always created like this: “new SequenceRange(0, -1)”, already meaning that, whatever happens, the values used in the range will be 0 and -1 (reason of the problem for you and me Mohsen).
    More than this, even if the getRowCount() method is implemented correctly and returns the complete quantity of rows contained in the dataModel (not just the quantity to display), the actually created rows will all be put in an ArrayList and the size() method will be used on it to know the real size of the dataSet. Whatever the getRowCount() implementation, only the rows actually fetched from the model will be summed in the end.

    I am using RichFaces v3.3.1. I’ll try to post something in the Jira to see what the people from RichFaces say.

    Nevertheless, it is more than probable that I have made something wrong in here. I’ll try to give an update if I have found a solution.

  18. Thank you a lot. Great ideas!

    I was wounded when I started to work with RF. Because it was impossible to disable internal sort and meantime use sorting pictures. It is realy ridiculous I can’t use sortable=”false” for disable internal sorting. Or any other way. Why UI component claws sorting function – it is impossible to understand.

    But. I found one solution for keep sort pictures! just instantinate stub:

    public class StubComparator implements Comparator {
    @Override
    public int compare(T o1, T o2) {
    //int index1 = wrappedKeys.indexOf(getId(o1));

    //int index2 = wrappedKeys.indexOf(getId(o2));

    return 0;
    }
    }

    set into comparator attribute and enjoy!

    Example1 (with custom order set/change):

    Example2 (with native look and feel):

    So. This solution is not beautiful but Jboss does not take another choice.

    Sorry for my bad english. And thanks a lot!!!

  19. There is one issue I can’t seem to figure out…getRowCount is always returning the same value as you page through the data because it is cached :(. I need to reset the row count each time they go to a new page(or if they click the refresh button on the browser as there may be new data and we don’t want to not grab that new data. AFter all the query itself is getting the new data as you page…so that looks a little flawed to me.

  20. Hello David,

    you wrote on July 23rd, 2008, that the columnName which should be sorted is provided to backing bean. I can see it too, when I debug the apps. BUT the database isn’t sorted. I can not see any method which is responsible for sorting. can you give me an advice, what should I do?

    thank you

  21. Eugune,

    The actual sorting is done in the database. In this line of the example code,

    for (FolderDTO folder : serviceForms.findFolders(firstRow, numberOfRows, sortField, descending)

    what’s happing is that it assumes that there is a serviceForms bean with a findFolders method, which takes the first row that should be returned, the number of rows to return, the field to sort on, and a boolean indicating whether the rows should be sorted ascending or descending. The service bean is what actually constructs the sql query to do that, and then it returns the appropriate results, which are used inside the for loop.

    David

  22. Suppose that I have somme search fields. These search fields are put in a class named ‘SearchCriteria’. And I have this method in backend to find the list of folders:

    serviceForms.findFolders(firstRow, numberOfRows, sortField, descending, searchCriteria);

    Do you know how I can pass my SearchCriteria in the walk() method of the ExtendedDataModel? By doing so, I can call my method above with searchCriteria to find out the folders.

  23. It’s very helpful to me. Thank you very much. I can use this for my application and send you the feedback if i will be having some problem.

    Thanks once again.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s