javaswingjtabletablerowsorterrowheader

How to handle RowSorter-Synchronization in JTables with RowHeader?


Following Situation: I have a J(X)Table with RowHeader (As guidline I used one of Rob Camicks great Examples). All worked as expected.

enter image description here

By requirement the data I receive from server already contains a tablerownumber, which I have to show in the rowheader and the data should be filterable. So I extended the example, and I added a filter. When I filtered the view I saw gaps in my row numbers (for example: 1, 3, 6,..), which is the desired effect.

To be able to filter and sort the table by my own tablerow, I added a TableRowSorter. And here I started to get confused. The Example uses the same TableModel and SelectionModel for mainTable and rowHeaderTable:

setModel( main.getModel() );
setSelectionModel( main.getSelectionModel() );

This is great, since I don’t have to synchronize them. But concerning TableRowSorter I suddenly wasn’t sure, if I also can or even have to use the same TableRowSorter-Instance or if I have to create a TableRowSorter for each table. First I added the same to both Tables, since this seemed practically, but then I got IndexOutOfBound-Exceptions in many cases. After some digging I found out that this is because the TableRowSorter gets updated twice at each TableModelEvent, because each table (RowHeader and MainTable) notifies the TableRowSorter about tablechanges on its own.

Now I am not sure which the right way to go is. Following solutions came into my mind: Should I add a second TableRowSorter (one for each table) and synchronize these, or should I wrap the TableModel within the RowHeaderTable and let it not fireing any Events? Or maybe I should create my own kind of RowHeaderTable which doesn’t notify Sorters about changes at all?


Solution

  • Here's a quick (beware: not formally tested! the usage example works fine, though) implementation of a wrapping RowSorter.

    It's client's responsibility to keep it in synch with the rowSorter used in the main table

    Usage example (in terms of SwingX test infrastructure and with SwingX sortController/table):

    public void interactiveRowSorterWrapperSharedXTable() {
        final DefaultTableModel tableModel = new DefaultTableModel(list.getElementCount(), 2) {
    
            @Override
            public Class<?> getColumnClass(int columnIndex) {
                return Integer.class;
            }
    
        };
        for (int i = 0; i < tableModel.getRowCount(); i++) {
            tableModel.setValueAt(i, i, 0);
            tableModel.setValueAt(tableModel.getRowCount() - i, i, 1);
        }
        final JXTable master = new JXTable(tableModel);
        final TableSortController<TableModel> rowSorter = (TableSortController<TableModel>) master.getRowSorter();
        master.removeColumn(master.getColumn(0));
        final JXTable rowHeader = new JXTable(master.getModel());
        rowHeader.setAutoCreateRowSorter(false);
        rowHeader.removeColumn(rowHeader.getColumn(1));
        rowHeader.setRowSorter(new RowSorterWrapper<TableModel>(rowSorter));
        rowHeader.setSelectionModel(master.getSelectionModel());
        // need to disable selection update on one of the table's 
        // otherwise the selection is not kept in model coordinates
        rowHeader.setUpdateSelectionOnSort(false);
        JScrollPane scrollPane = new JScrollPane(master);
        scrollPane.setRowHeaderView(rowHeader);
        JXFrame frame = showInFrame(scrollPane, "xtables (wrapped sortController): shared model/selection");
        Action fireAllChanged = new AbstractAction("fireDataChanged") {
    
            @Override
            public void actionPerformed(ActionEvent e) {
                tableModel.fireTableDataChanged();
            }
    
        };
        addAction(frame, fireAllChanged);
        Action removeFirst = new AbstractAction("remove firstM") {
    
            @Override
            public void actionPerformed(ActionEvent e) {
                tableModel.removeRow(0);
    
            }
        };
        addAction(frame, removeFirst);
        Action removeLast = new AbstractAction("remove lastM") {
    
            @Override
            public void actionPerformed(ActionEvent e) {
                tableModel.removeRow(tableModel.getRowCount() - 1);
    
            }
        };
        addAction(frame, removeLast);
        Action filter = new AbstractAction("toggle filter") {
    
            @Override
            public void actionPerformed(ActionEvent e) {
                RowFilter filter = rowSorter.getRowFilter();
                if (filter == null) {
                    rowSorter.setRowFilter(RowFilter.regexFilter("^1", 1));
                } else {
                    rowSorter.setRowFilter(null);
                }
    
            }
        };
        addAction(frame, filter);
        addStatusMessage(frame, "row header example with RowSorterWrapper");
        show(frame);
    }
    

    The RowSorterWrapper:

    /**
     * Wrapping RowSorter for usage (f.i.) in a rowHeader.
     * 
     * Delegates all state queries, 
     * does nothing on receiving notification of model changes,
     * propagates rowSorterEvents from delegates.
     * 
     * Beware: untested! 
     * 
     * @author Jeanette Winzenburg, Berlin
     */
    public class RowSorterWrapper<M> extends RowSorter<M> {
    
        private RowSorter<M> delegate;
        private RowSorterListener rowSorterListener;
    
        public RowSorterWrapper(RowSorter<M> delegate) {
            this.delegate = delegate;
            delegate.addRowSorterListener(getRowSorterListener());
        }
    
        /**
         * Creates and returns a RowSorterListener which re-fires received
         * events.
         * 
         * @return
         */
        protected RowSorterListener getRowSorterListener() {
            if (rowSorterListener == null) {
                RowSorterListener listener = new RowSorterListener() {
    
                    @Override
                    public void sorterChanged(RowSorterEvent e) {
                        if (RowSorterEvent.Type.SORT_ORDER_CHANGED == e.getType()) {
                            fireSortOrderChanged();
                        } else if (RowSorterEvent.Type.SORTED == e.getType()) {
                            fireRowSorterChanged(null);                }
                    }
                };
                rowSorterListener = listener;
            }
            return rowSorterListener;
        }
    
    
        @Override
        public M getModel() {
            return delegate.getModel();
        }
    
        @Override
        public void toggleSortOrder(int column) {
            delegate.toggleSortOrder(column);
        }
    
        @Override
        public int convertRowIndexToModel(int index) {
            return delegate.convertRowIndexToModel(index);
        }
    
        @Override
        public int convertRowIndexToView(int index) {
            return delegate.convertRowIndexToView(index);
        }
    
        @Override
        public void setSortKeys(List keys) {
            delegate.setSortKeys(keys);
        }
    
        @Override
        public List getSortKeys() {
            return delegate.getSortKeys();
        }
    
        @Override
        public int getViewRowCount() {
            return delegate.getViewRowCount();
        }
    
        @Override
        public int getModelRowCount() {
            return delegate.getModelRowCount();
        }
    
        @Override
        public void modelStructureChanged() {
            // do nothing, all work done by delegate
        }
    
        @Override
        public void allRowsChanged() {
            // do nothing, all work done by delegate
        }
    
        @Override
        public void rowsInserted(int firstRow, int endRow) {
            // do nothing, all work done by delegate
        }
    
        @Override
        public void rowsDeleted(int firstRow, int endRow) {
            // do nothing, all work done by delegate
        }
    
        @Override
        public void rowsUpdated(int firstRow, int endRow) {
            // do nothing, all work done by delegate
        }
    
        @Override
        public void rowsUpdated(int firstRow, int endRow, int column) {
            // do nothing, all work done by delegate
        }
    
    }