November 15, 2009 - zenoconsulting
Table of Contents
|
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:
- com.google.gwt.gen2.table.client.PagingScrollTable
- com.google.gwt.widgetideas.table.client.PagingScrollTable
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 one blog page
Tags
hi Davis,
Thanks for this post..
how do you maintain sorted data between pages if you have paging???
I am facing problems in this regard..
In your project that will be moved to production have you implented pagination?
thanks
chythanya
Hi Chythanya,
Did you try the live demo above? It is doing pagination. There is a page widget at the bottom of the scroll table. Sorting works fine between the pages.
Exactly what i have been looking for .. thnx a lot :) worked perfectly for me !!
yes Davis, Sorting works fine between the pages.
This is an Excellent tutorial for using Pagingscrolltable with Sorting.
Thanks a lot for this post :)
Hello Daid, .. It runs fast . Very good.
I ran into a issue here …Here is the scenario. I am using a ListCellEditor, After selecting new value OnClick of "Tick " , I need to update DB and then only I should change it in UI. I am overriding onSetRowValue() to make the RPC call . Is there a way to control the onSetRowValue() return based on the RPC call ? Right now what is happening is onSetRowValue() returns without waiting for RPC to come back. So if i return true , and my DB if it did not get update due to any reason ,my screen value is not correct.
Hope I am the scenario clear.
Thanks
Joe
Is there a way to put the images that appear when you click the table's header columns next to the headers rather than underneath them?
Thanks
I think this is possible. I'd look at the CSS file that goes along with the PagingScrollTable. You'll find it in my maven project. I'm willing to bet that there is where it defines the up/down arrow placement images, and if you're a CSS guru, you can probably get it to do what you want.
Hi Joe, I'm not 100% clear on what you want, but I think what you are trying to do is have one of the columns in the table use ListCellEditor so it provides a drop-down list the user can pick from. Then, when the user selects a value from the list, you want to make an RPC call to update the database, and if it returns ok, refresh the table row so it shows that selection.
I'm a little confused maybe — what would you show otherwise? If the user selects from a drop down list, that is the selection. It could trigger RPC, but you wouldn't change it would you? I think I'd prefer to do it something like this:
Does that make sense? I haven't tried overriding onSetRowValue(), so I can't really comment on it, but there are a multitude of other hooks where you could likely tie into. The ListCellEditor takes a ListBox, right? There are all kinds of places you can hook into that: ListBox
Please can I make use of this code in my programs?
Yep - there is no license, so feel free to use it, modify it — do whatever you wish.
Post preview:
Close preview