GWT Paging Scroll Table

November 15, 2009 - zenoconsultingzenoconsulting


Introduction

I've been doing a lot of development lately with the Google Web Toolkit. One of the things I clamored for was a decent grid or table that had good performance, sortable columns, and pagination. Surprisingly, there are few options out there (to date), for GWT developers. There are some standard widgets in GWT like FlexTable and Grid, and these widgets work great for small tasks, but they don't support pagination, scrolling, or large data sets. GWT also has a ScrollPanel, but again that lacks pagination and support for large data sets.

When I say large data sets, I tend to mean hundreds, perhaps thousands of rows. You may chuckle and consider that small, but if you realize that you have to load this in the DOM dynamically and present it in the browser, thousands of rows is stretching the performance limitations of today's browser without implementing some kind of pagination between the client and server.

You may decide that you absolutely need pagination between client and server because your data sets are just that large. In my case, if I could find a way to load thousands of rows with reasonable performance, then I didn't need to go back to the server for more data. I found that solution in the gwt-incubator's PagingScrollTable. This widget really flies, especially if you pair it up with the BulkTableRenderers (see the link for some impressive numbers). You can try the demo for yourself below. In my experience, if I were to build a similar example using the Grid or FlexTable or many of the other 3rd party widgets I tried, I would get horrible performance for even hundreds of rows. Especially in IE — sometimes it took several minutes to render even 100 rows.

The PagingScrollTable can be a bit confusing to use at first. When I started using it, documentation was sparse, and to make matters worse, there were two versions in gwt-incubator:

The latter is deprecated. Use the former. The API for setting this up is a bit confusing, and others have asked questions about this widget on the GWT list before, so I thought I'd create a simple example that shows how I'm using it — comments welcome ;)

Disclaimer

I'm just throwing this sample code / demo out here for the GWT-consuming public. It may very well be that I'm doing something goofy here with the incubator API. It seems to work very well for me, however, and it became kind of a set-and-forget after that. Your mileage may vary.

Live Demo

Here's a demo of the widget in action. Feel free to play around with it. Feel free to even enter a ridiculously large number for the row count and watch your browser hang. :) You can sort any of the columns by clicking on the column header (ascending or descending). You can also select a single row which will bring up a dialog box with the message object's content.

Code

You can download a fully-working maven GWT project of this demo here. Install maven if you don't have it. You should be able to run the comand mvn -version successfully. I use Eclipse as an IDE. If you use something different, maven typically has commands/plugins to generate project files for other popular IDE's (e.g. netbeans, intellij, etc.)

How to Import into Eclipse

Run

$ mvn eclipse:eclipse

Open Eclipse —> File —> Import —> Exsiting Projects Into Workspace

How to Run from Command Line

$ mvn gwt:run

This should spawn the GWT Hosted Mode Browser Windows at the app's URL http://localhost:8888/com.example.Application/Application.html

How To Put It Together

Define a Model Class

First, you need a typical model class. This will represent a row in your table. Mine is a very stupid class called Message with three properties for id, text, and date. All of the GWT table classes related to PagingScrollTable ask you to parameterize them with this class type. Here's the bean (getters/setters, etc. excluded). Remember that if you intend to serialize it through GWT-RPC, you have to make is Serializable and it needs a default no-arg constructor.

public class Message {
 
    private long id;
    private Date date;
    private String text;
 
    /**
     * @param id
     * @param text
     * @param date
     */
    public Message(long id, String text, Date date) {
        super();
        this.id = id;
        this.date = date;
        this.text = text;
    }

Extend MutableTableModel with your Model type

This serves the data via an Iterator. You define how that iterator is populated. I chose to create a public setData(ArrayList<Message> list) method so I could update the data anytime I wanted. Of course, you might decide to go fetch it from GWT-RPC or some other web service, etc. I would put the code that fetches the data in a Presenter class, and just have it stuff this ArrayList into the UI as necessary. I use ArrayList so the GWT compiler doesn't have to guess.

I'm throwing the list away and storing it in a map instead, indexed by id. This is just a convenience so I can get a message from the map on demand when a user clicks on a row — this provides an easy reference if the user wanted to select a row, edit a message, and save their changes. This message class is so simple, you could throw away the map and let it get garbage collected, since you could just recreate a new Message object on demand from the string values inside the HTML table, but…you can optimize that out later if you wish. Keeping the map around makes life simpler for now.

I also defined my own MessageSorter that deals with sorting Messages (shown below). Basically, I just map the column index the user clicked on to a specific Comparator, sort the map, and return a sorted iterator. It is pretty simple, and I'm sure there are other (better) ways to do this, but it seems to work well for me.

        /**
     * Extension of {@link MutableTableModel} for our own {@link Message} type.
     */
    private class DataSourceTableModel extends MutableTableModel<Message> {
 
        // knows how to sort messages
        private MessageSorter sorter = new MessageSorter();
 
        // we keep a map so we can index by id
        private Map<Long, Message> map;
 
        /**
         * Set the data on the model.  Overwrites prior data.
         * @param list
         */
        public void setData(ArrayList<Message> list) {
            // toss the list, index by id in a map.
            map = new HashMap<Long, Message>(list.size());
            for(Message m : list) {
                map.put(m.getId(), m);
            }
        }
 
        /**
         * Fetch a {@link Message} by its id.
         * @param id
         * @return
         */
        public Message getMessageById(long id) {
            return map.get(id);
        }
 
        @Override
        protected boolean onRowInserted(int beforeRow) {
            return true;
        }
 
        @Override
        protected boolean onRowRemoved(int row) {
            return true;
        }
 
        @Override
        protected boolean onSetRowValue(int row, Message rowValue) {
            return true;
        }
 
        @Override
        public void requestRows(
                final Request request,
                TableModel.Callback<Message> callback) {
 
            callback.onRowsReady(request, new Response<Message>(){
 
                @Override
                public Iterator<Message> getRowValues() {
                    final int col = request.getColumnSortList().getPrimaryColumn();
                    final boolean ascending = request.getColumnSortList().isPrimaryAscending();
                    if(col < 0) {
                        map = sorter.sort(map, new IdComparator(ascending));
                    } else {
                        switch(col) {
                        case 0:
                            map = sorter.sort(map, new IdComparator(ascending));
                            break;
                        case 1:
                            map = sorter.sort(map, new TextComparator(ascending));
                            break;
                        case 2:
                            map = sorter.sort(map, new DateComparator(ascending));
                            break;
                        }
                    }
                    return map.values().iterator();
                }});
        }
 
    }

Here's the MessageSorter class — nothing too exciting about this. I implemented a Comparator for each property I wanted sorted on the Message class. I'm only showing the id comparator here.

    /**
     * Sort the incoming map via the comparator.
     * @param map the map to sort
     * @param comparator the comparator to use
     * @return a sorted map.
     */
    public Map<Long, Message> sort(Map<Long, Message> map, Comparator<Message> comparator) {
        final List<Message> list = new LinkedList<Message>(map.values());
        Collections.sort(list, comparator);
        Map<Long, Message> result = new LinkedHashMap<Long, Message>(list.size());
        for(Message p : list) {
            result.put(p.getId(), p);
        }
        return result;
    }
 
    /**
     * {@link Comparator} for sorting by {@link Message} ID
     */
    public final static class IdComparator implements Comparator<Message> {
 
        private final boolean ascending;
 
        public IdComparator(boolean ascending) { this.ascending = ascending; }
 
        @Override
        public int compare(Message m1, Message m2) {
            final Long id1 = m1.getId(); 
            final Long id2 = m2.getId(); 
            if(ascending) { 
                return id1.compareTo(id2);
            } else { 
                return id2.compareTo(id1);
            }
        }
    }

Extend AbstractColumnDefinition for each Column you want

Here's an example of doing this for the DATE column. I did not make the cells editable, so I did not bother with the setter. You have to create one of these for each column that defines how to extract the information out of your model object. This is slightly annoying, but does present a useful hook if you want to re-format the data like I am doing here with the DateColumnDefinition. Instead of just taking java.util.Date's default toString() implementation, I format it to something more human-readable.

    /**
     * Defines the column for {@link Message#getStartDate()} 
     */
    private final class DateColumnDefinition extends AbstractColumnDefinition<Message, String> {
        @Override
        public String getCellValue(Message rowValue) {
            return new SimpleDateFormat("MM/dd/yyyy").format(rowValue.getDate());
        }
        @Override
        public void setCellValue(Message rowValue, String cellValue) { }
    }

Create Your TableDefinition

Now that you have your columns defined, and you defined how to serve data to/from it. You define the table itself, which specifies things like which columns, how wide, is it sortable, truncatable, etc. I'm only showing the id column being set up here. You have to do this for all the columns you want to show in your table.

private DefaultTableDefinition<Message> createTableDefinition() {
        tableDefinition = new DefaultTableDefinition<Message>();
 
        // set the row renderer
        final String[] rowColors = new String[] { "#FFFFDD", "EEEEEE" };
        tableDefinition.setRowRenderer(new DefaultRowRenderer<Message>(rowColors));
 
        // id
        {
            IdColumnDefinition columnDef = new IdColumnDefinition();
            columnDef.setColumnSortable(true);
            columnDef.setColumnTruncatable(false);
            columnDef.setPreferredColumnWidth(35);
            columnDef.setHeader(0, new HTML("Id"));
            columnDef.setHeaderCount(1);
            columnDef.setHeaderTruncatable(false);
            tableDefinition.addColumnDefinition(columnDef);
        }

Create PagingScrollTable

This ties all the pieces together. The real magic happens by using FixedWidthGridBulkRenderer. Try removing it and see what happens. I'm not showing all the code here. Most of it isn't super interesting. You can download the source, and see all of it together.

    /**
     * Initializes the scroll table
     * @return
     */
    private PagingScrollTable<Message> createScrollTable() {
        // create our own table model
        tableModel = new DataSourceTableModel();
        // add it to cached table model
        cachedTableModel = createCachedTableModel(tableModel);
 
        // create the table definition
        TableDefinition<Message> tableDef = createTableDefinition();
 
        // create the paging scroll table
        PagingScrollTable<Message> pagingScrollTable = new PagingScrollTable<Message>(cachedTableModel, tableDef);
        pagingScrollTable.setPageSize(50);
        pagingScrollTable.setEmptyTableWidget(new HTML("There is no data to display"));
        pagingScrollTable.getDataTable().setSelectionPolicy(SelectionPolicy.ONE_ROW);
 
        // setup the bulk renderer
        FixedWidthGridBulkRenderer<Message> bulkRenderer = new FixedWidthGridBulkRenderer<Message>(pagingScrollTable.getDataTable(), pagingScrollTable);
        pagingScrollTable.setBulkRenderer(bulkRenderer);
 
        // setup the formatting
        pagingScrollTable.setCellPadding(3);
        pagingScrollTable.setCellSpacing(0);
        pagingScrollTable.setResizePolicy(ScrollTable.ResizePolicy.FILL_WIDTH);
 
        pagingScrollTable.setSortPolicy(SortPolicy.SINGLE_CELL);
        pagingScrollTable.getDataTable().addRowSelectionHandler(rowSelectionHandler);
 
        return pagingScrollTable;
    }

Final Steps

I wrapped all this in a standard class that extends GWT's Composite class, and added a public method to stuff data into it:

    /**
     * Allows consumers of this class to stuff a new {@link ArrayList} of {@link Message}
     * into the table -- overwriting whatever was previously there.
     * 
     * @param list the list of messages to show
     * @return the number of milliseconds it took to refresh the table
     */
    public long showMessages(ArrayList<Message> list) {
        long start = System.currentTimeMillis();
        // update the count
        countLabel.setText("There are "+ list.size() + " messages.");
        // reset the table model data
        tableModel.setData(list);
        // reset the table model row count
        tableModel.setRowCount(list.size());
        // clear the cache
        cachedTableModel.clearCache();
        // reset the cached model row count
        cachedTableModel.setRowCount(list.size());
        // force to page zero with a reload
        pagingScrollTable.gotoPage(0, true);
        long end = System.currentTimeMillis();
        return end - start;
    }

Finally, I create a class that provides a main EntryPoint, and I wrapped it with the stuff to generate mock rows of data, etc. Nothing too exciting there.

That's pretty much it — have fun!


Backlinks


Add a New Comment
or Sign in as Wikidot user
(will not be published)
- +
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License