Tables


The CoursesPanel JList shows a single string for each Course object. This presentation is adequate because you only display two pieces of data: the department and course number. Adding the new effective date attribute to the summary string, however, would quickly make the list look jumbled. The JList control, in fact, works best when you have only a single piece of data to represent per row in the list.

A JTable is a very effective control that allows you to present each object as a sequence of columns. A JTable can look a lot like a spreadsheet. In fact, JTable code and documentation uses the same terms as spreadsheets: rows, columns, and cells.

The JTable class gives you a considerable amount of control over look and feel. For example, you can decide whether or not you want to allow users to edit individual cells in the table, you can allow the user to rearrange the columns, and you can control the width of each column.

For this exercise, you'll replace the JList with a JTable. The best place to start is to create the data model that will underly the JTable. Just as you inserted Course objects into a list model for the JList, you will put Course objects into a model that you attach to the JTable.

Creating the JTable model is slightly more involved. The JList was able to get a printable representation by sending the toString message to each object it contained. The JTable must treat each attribute for a given object separately. For every cell it must display, the JTable sends the message getValueAt to the model. It passes the row and column representing the current cell. The getValueAt method must return a string to display at this location.

There would be no easy way for the model to figure out what attribute you want to display for a given column. As such, you must provide a model implementation yourself. You must supply the getValueAt method in this implementation, as well as two other methods: getrowCount and getColumnCount. To enhance the look and feel of the table, you will likely implement other methods. The test below, CoursesTableModelTest, shows that the model implements the method getColumnName to return a text header for each column.

 package sis.ui; import junit.framework.*; import sis.studentinfo.*; import sis.util.*; import java.util.*; public class CoursesTableModelTest extends TestCase {    private CoursesTableModel model;    protected void setUp() {       model = new CoursesTableModel();    }    public void testCreate() {       assertEquals(0, model.getRowCount());       assertEquals(3, model.getColumnCount());       FieldCatalog catalog = new FieldCatalog();       Field department =           catalog.get(FieldCatalog.DEPARTMENT_FIELD_NAME);       assertEquals(department.getShortName(), model.getColumnName(0));       Field number = catalog.get(FieldCatalog.NUMBER_FIELD_NAME);       assertEquals(number.getShortName(), model.getColumnName(1));       Field effectiveDate =           catalog.get(FieldCatalog.EFFECTIVE_DATE_FIELD_NAME);       assertEquals(effectiveDate.getShortName(),                     model.getColumnName(2));    }    public void testAddRow() {       Course course = new Course("CMSC", "110");       course.setEffectiveDate(DateUtil.createDate(2006, 3, 17));       model.add(course);       assertEquals(1, model.getRowCount());       final int row = 0;       assertEquals("CMSC", model.getValueAt(row, 0));       assertEquals("110", model.getValueAt(row, 1));       assertEquals("03/17/06", model.getValueAt(row, 2));    } } 

The test presents another use for the FieldCatalogreturning an appropriate column header for each field. It uses a new Field attribute with the more abstract concept of a "short name," an abbreviated name for use in -constrained display spaces. You'll need to update Field and FieldCatalog/FieldCatalogTest to supply this new information.

The easiest way to build a table model is to subclass javax.swing.table.AbstractTableModel. You then need only supply definitions for getValueAt, geTRowCount, and getColumnCount. You will want to store a collection of courses in the model. You'll need a method (add) that allows adding a new Course to the model.

You can also choose to work with javax.swing.table.DefaultTableModel. Sun provided this concrete implementation to make your job a bit simpler. However, DefaultTableModel requires that you organize your data first (in the form of either Vector objects or Object arrays) and then pass it to the model. It's almost as easy, and ultimately more effective, to create your own AbstractTableModel subclass.

 package sis.ui; import javax.swing.table.*; import java.text.*; import java.util.*; import sis.studentinfo.*; class CoursesTableModel extends AbstractTableModel {    private List<Course> courses = new ArrayList<Course>();    private SimpleDateFormat formatter =       new SimpleDateFormat("MM/dd/yy");    private FieldCatalog catalog = new FieldCatalog();    private String[] fields = {       FieldCatalog.DEPARTMENT_FIELD_NAME,       FieldCatalog.NUMBER_FIELD_NAME,       FieldCatalog.EFFECTIVE_DATE_FIELD_NAME };    void add(Course course) {       courses.add(course);       fireTableRowsInserted(courses.size() - 1, courses.size());    }    Course get(int index) {       return courses.get(index);    }    public String getColumnName(int column) {       Field field = catalog.get(fields[column]);       return field.getShortName();    }    // abstract (req'd) methods: getValueAt, getColumnCount, getRowCount    public Object getValueAt(int row, int column) {       Course course = courses.get(row);       String fieldName = fields[column];       if (fieldName.equals(FieldCatalog.DEPARTMENT_FIELD_NAME))          return course.getDepartment();       else if (fieldName.equals(FieldCatalog.NUMBER_FIELD_NAME))          return course.getNumber();       else if (fieldName.equals(FieldCatalog.EFFECTIVE_DATE_FIELD_NAME))          return formatter.format(course.getEffectiveDate());       return "";    }    public int getColumnCount() {       return fields.length;    }    public int getRowCount() {       return courses.size();    } } 

Note the subtle redundancies that abound. The table must contain a list of fields you are interested in displaying. In getValueAt, you obtain the field name at the column index provided. You use this name in a pseudoswitch statement to determine which Course getter method to call. A shorter bit of code would involve a switch statement:

 switch (column) {    case 0: return course.getDepartment();    case 1: return course.getNumber();    case 2: return formatter.format(course.getEffectiveDate());    default: return ""; } 

While it is acceptable, I dislike the disconnect between the column number and the attribute. Changes to the columns or their order can easily result in errors. (At least your tests would catch the problem.) But see the sidebar for a discussion of the solution I present.

You will, of course, need to make a few more changes to get the JTable working. In CoursesPanelTest:

 public void testCreate() {    assertEmptyTable(COURSES_TABLE_NAME);    assertButtonText(ADD_BUTTON_NAME, ADD_BUTTON_TEXT);    ... } public void testAddCourse() {    Course course = new Course("ENGL", "101");    panel.addCourse(course);    JTable table = panel.getTable(COURSES_TABLE_NAME);    CoursesTableModel model = (CoursesTableModel)table.getModel();    assertSame(course, model.get(0)); } private void assertEmptyTable(String name) {    JTable table = panel.getTable(name);    assertEquals(0, table.getModel().getRowCount()); } 

Domain Maps

The implementation of getValueAt contains significant redundancy. You test the selected field name against each possible field name in order to obtain the corresponding attribute from the Course object. A more radical solution is to implement your domain objects as hash tables. The hash table key is the attribute name. You would set the value in the Course like this:

 course.set(DEPARTMENT_FIELD_NAME, "CMSC"); 

And you would retrieve the value like this:

 String courseName = (String)course.get(DEPARTMENT_FIELD_NAME); 

or

 String courseName = course.getString(DEPARTMENT_FIELD_NAME); 

The code in getValueAt becomes very succinct:

 Course course = courses.get(row); return course.get(fields[column]); 

This solution can greatly diminish the redundancy in your system, but it introduces other concerns. First is the decreased readability when you attempt to follow the code or debug it. Also, the solution would require casting, either at the point of call or embedded within a bunch of utility methods such as getString, geTDate, and getInt.


In CoursesPanel:

 static final String COURSES_TABLE_NAME = "coursesTable"; ... private void createLayout() {    JTable coursesTable = createCoursesTable();    JScrollPane coursesScroll = new JScrollPane(coursesTable);    coursesScroll.setVerticalScrollBarPolicy(       ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);    ... } private JTable createCoursesTable() {    JTable table = new JTable(coursesTableModel);    table.setName(COURSES_TABLE_NAME);    table.setShowGrid(false);    table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);    return table; } void addCourse(Course course) {    coursesTableModel.add(course); } Course getCourse(int index) {    return coursesTableModel.get(index); } 

You can remove the class CourseDisplayAdapter and any references to the old courses list.

Make sure you take a look at the Java API documentation for JTable and the various table model classes. The JTable class contains quite a bit of customization capabilities.



Agile Java. Crafting Code with Test-Driven Development
Agile Javaв„ў: Crafting Code with Test-Driven Development
ISBN: 0131482394
EAN: 2147483647
Year: 2003
Pages: 391
Authors: Jeff Langr

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net