Bennett Business Solutions, Inc. - Technical Consulting
Home > Articles > JTables with Class


JTables with Class - MVC, Renderers, and the TableSorter

JTable and its associated classes offer a powerful and cleanly designed implementation of table functionality. The Model - View - Controller design manifests itself in several useful ways, and the TableSorter class provided by Sun makes table sorting possible with a trivial amount of effort.

Separation of the table view from the table model

In a classic implementation of the observer pattern, the table view listens to events sent to it by the table model. This enables multiple views (tables or otherwise) to use the same underlying table model.

The model can send to its listeners change notifications of varying levels of granularity: single cell, row, all data, and table structure.

The result of this clean separation is that plugging a different data set into a table is as easy as calling the JTable's setModel() call with the new model. Another benefit is the resulting efficient division of labor (codewise and peoplewise), and clean organization and coordination of multiple model implementations.

Custom Cell Renderers

The drawing of table cells is also done using the MVC approach. The object to be displayed in the cell comes from a call to the model's getValueAt() method. The presentation of the data is governed by a cell renderer, an object that provides a component used to draw the visual content of a table cell. Default renderers provided by the Java library display boolean values as JCheckBoxes, and all other data types as Strings in JLabels.

Although technically any Component can be used by a renderer to display your value, use of anything other than a JLabel is rare. Nevertheless, even a JLabel can enable you to provide functionality beyond that of the default behavior. For example, you would define a JLabel custom renderer to:

  1. Display a Date or other non-Boolean object in a format other than its toString() representation.

    The Date class toString() method returns a long formatted string in the English language. If you want to use a different format or support non-English languages, you need to provide a renderer to do so.

  2. Display an icon instead of, or in addition to, text.

    A picture is worth a thousand words -- icons can provide far more communicative value per pixel, saving valuable screen real estate and making your interface more attractive as a side benefit. For example, for a table column depicting levels of a dangerous substance, you could provide a renderer that contained a color coded circle alongside the quantity string, with the color red signifying the most dangerous level. Better yet would be to provide icons whose shapes are also different so that the distinctions would be clear to color blind users and in black and white printouts.

  3. Display a human-comprehensible representation of a less human-meaningful value, such as when an enumeration, number, or string abbreviation is used as a lightweight token for a more complex or meaningful value.

    For example, let's say your SalesOrder class contains a 'status' member that (to oversimplify) can contain one of the states ordered, shipped, billed, or paid. These will probably be stored as numbers, enumerations, or string abbreviations. A renderer would be used to generate a human-meaningful display string based on the stored value.

    Another reason to use a renderer is to support multiple languages when you want the table to sort on that column identically regardless of the language used. In the previously cited example, there is a natural chronological order to "ordered", "shipped", "billed", and "paid". You would probably want that to be the natural order, regardless of the alphabetical order of the display strings.

    Sorters will use the values returned by the table model's getValueAt() method to determine the sort order. Therefore, it is necessary to have getValueAt() return the underlying object, and have the renderer generate the display string.

    In order to allow your custom sort order to work, you need to ensure that the underlying data object (i.e. the object returned by getValueAt()) implements the Comparable interface and its compareTo() method sorts in the desired order.

When the user clicks an editable table cell, the cell's renderer no longer controls the appearance or behavior of that cell. Instead, a cell editor is used to handle the user input. The default cell editor is a JCheckBox for boolean values, and a JTextField for all other types of values. Again, Swing allows you to override this default behavior; you can specify any Component as a custom editor.

The TableSorter

This is all great, but there's something missing from Swing's built-in behavior. Inevitably, and with good reason, your users will ask "how can I get the table to sort by a given column or set of columns?"

Fortunately, the solution is simple. Although it's not in the Java libraries, Sun has kindly provided for us a file named TableSorter.java that addresses this need. It can be found at:

http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/TableSorter.java

At almost five hundred lines, the file is rather large. It contains several helper classes defined within the TableSorter class: Row, TableModelHandler, MouseHandler, Arrow, SortableHeaderRenderer, and Directive.

The first version of TableSorter was written in 1997. According to the Javadoc, Version 2 is dated February 27, 2004, and its authors are Philip Milne, Brendon McLean, Dan van Enckevort, and Parwinder Sekhon.

When TableSorter is used in conjunction with JTable, the user can click on a column heading, and the data will be sorted by the values in that column. Each click will cycle the column through ascending sort, descending sort, and no sort. Furthermore, it is possible to sort by multiple columns (called "compound sort" in the Javadoc) by holding down the Ctrl key while clicking the subsequent column headings.

As provided, this class resides in the default package (in other words, the source does not contain a package statement). You will probably want to put it in a package appropriate to your environment. The class is simple to use, as illustrated by the sample program TableSorterTest.java. The real functionality does not begin until halfway through the file; the first half or so is just setup of sample data.

The relevant lines of source are in the TableSorterTest constructor:

   TableSorter sorter = new TableSorter(innerTableModel);
   table = new JTable(sorter);
   JTableHeader header = table.getTableHeader();
   sorter.setTableHeader(header);
                  

Here is a window produced by TableSorterTest. This table is sorted in ascending date order.

Dates with DefaultTableModel

...or is it? Can you see the problem? The dates sort by the alphabetical order of their representations, and not chronologically by the dates' values.

Why is this? Because DefaultTableModel treats all table cell values as Objects, and the Object class does not implement the Comparable interface. The TableSorter code shows us that if the column's class is not Comparable, then a lexical comparator that uses values returned by the Object's toString() method is used in sorting. This is why the Date values are sorted in alphabetical and not chronological order.

As is usually the case, we find that it is unwise to use the DefaultTableModel, especially without subclassing it. The simplest way to fix our problem is to subclass the DefaultTableModel, adding to it the ability to report the class type of each column. To minimize the additional code, we use Java's shorthand notation to define and instantiate a subclass:

    return new DefaultTableModel(
        config.tableData, config.columnNames) {
        
        Class [] classes = config.classes;
            
        public Class getColumnClass(int column) {
            return classes[column];
        }
    };
                  

The config object is used merely for convenience to encapsulate table configuration information. Its class, TableConfigData, is a static inner class in TableSorterTest.

After making this fix, and clicking on the column heading as before, we get the following table:

Dates with subclass of DefaultTableModel

The values are both formatted and sorted differently. I neglected to tell you that in order to create a more dramatic illustration, I inserted the following as the first line in TableSorterTest's main():

    Locale.setDefault(Locale.GERMANY);
                      

Internally, the default renderer for columns with Date objects uses a java.text.DateFormat instance to format the date in a manner appropriate for the default (in this case German) locale. The JDK 1.4.2_03 source code of JTable shows the following definition of a nested class used to render Dates:

    static class DateRenderer extends DefaultTableCellRenderer {
        DateFormat formatter;
        public DateRenderer() { super(); }

        public void setValue(Object value) {
            if (formatter==null) {
                formatter = DateFormat.getDateInstance();
            }
            setText((value == null) ? "" : formatter.format(value));
        }
    }
                      

This class is an inner class of JTable, as shown by the output of the renderer's toString() method in TableSorterTest:

Column #0's renderer is :
javax.swing.JTable$DateRenderer[,0,0,0x0,invalid,alignmentX=0.0,
alignmentY=null,border=javax.swing.border.EmptyBorder@6f7ce9,flags=8,
maximumSize=,minimumSize=,preferredSize=,defaultIcon=,disabledIcon=,
horizontalAlignment=LEADING,horizontalTextPosition=TRAILING,
iconTextGap=4,labelFor=,text=,verticalAlignment=CENTER,
verticalTextPosition=CENTER]
The German locale requires DD.MM.YYYY format. Also, since the table knows that these objects are Dates, and since the TableSorter knows that Dates are Comparable, the Date.compareTo() method is used to sort the column's values.

Unfinished Business

While the TableSorter class is extremely useful, there are a couple of things that could make it even better. I have modified the original TableSorter.java file to address these issues. This new file can be found here. The example windows above were created using this modified version of the file.

  • Comparing Strings

    TableSorter uses the String class' compareTo() method. This method tests the Unicode value of the characters of both strings in sequence. However, this approach is too simplistic in most cases. For example:

    • It does not support case insensitive sorts.
    • Some languages use characters whose sort order differs from the order of the numeric values in the character set. For example, in German, accented vowels are sorted adjacent to their unaccented counterparts, even though their values are greater than that of the letter 'z'.
    • Some characters in certain languages may assume different sort positions based on their context in the surrounding string. Thai, for example, has a character that means "repeat the last syllable".

    Instead, the java.text.Collator and its associated classes should be used. As long as Java supports the locale in use, you get collation support for the cost of some more computation time.

  • The Arrow Icon

    The arrow icon used to indicate the column's sort direction is difficult to discern. I think it would be more legible if it were a bit larger, and if it were rendered in a solid color (2D rather than 3D).

Here are some screen shots to illustrate the difference. First, using the original TableSorter:

Strings using the original table model.

Then, using the NewTableSorter:

Strings using the new table model.

Notice the difference in the arrows. The arrow in the sample using the original TableSorter is barely visible, whereas the arrow in the sample using the NewTableSorter is virtually impossible to miss.

In addition, the sort order in the upper sample is by Unicode value, whereas the sort order in the lower sample is more human-comprehensible and is, in fact, the required sort order for the German locale.

By the way, here is an example of a multicolumn sort. Notice how the size of the arrow indicates the priority of that column in the sort.

MVC Degeneration?

The MVC approach dictates that a model have no knowledge of the view (other than that the view is a listener to change events from the model). However, the TableSorter is determining the row numbers of the respective objects. This is really a view function, not a model function.

Also, an assumption of models is that a single model can drive multiple views. However, if the user wanted to display the same data in different tables with different sort orders, then it would not be possible to use a single TableSorter.

Therefore, although TableSorter extends TableModel, it is really a class performing a view function, not a model function.

Conclusion

Swing's JTable, combined with the supplementary TableSorter class, provides a rich and flexible toolkit for building sophisticated tables in your application or applet. Although an initial investment of time is required to become familiar with these classes, once that investment is made, you will be able to build tables with class, quickly and effectively.

About the Author

Keith Bennett photo

Keith R. Bennett is a Sun Certified Java Programmer with over twenty years experience in software development using a wide variety of languages, tools, and operating systems. He is currently building Bennett Business Solutions, Inc., a technical consulting company in Reston, Virginia, in the Washington, DC metropolitan area.

 

 

back to top