javaswingjtabletablerowsorter

Autosort JTable after dataModelUpdate


Keep sorting after data model changes.

Expected behaviour: I click one of the columnheaders to sort, and Its supposed to keep sorting that column as the dataModel changes. That is, once you choose a column to sort by, you shouldn't have to click on it manually after each update of the underlying data model to see it in that order.

Current Behaviour: Seems to reset the sorting after data model changes. That is, as if I havent clicked any column. Update of data model makes it jumbled again.

What I tryed: I wrote a small program that mimics the change of the underlying datamodel through a for loop, every 2000 milliseconds. Since the original data changes so much from update to update, I chose to change the data with DefaultTableModel.setDataVector(), instead of row by row which seems bulky if lots of rows change. I used what made sense to me, that is TableRowSorter.setSortsOnUpdates(), to well... sort after updates x). I left commented out code to show that I tryed different approaches, some found here on stackexchange, but couldnt make it work anyway. I tryed reading into the api and found this as a possible problemmaker:

If the underlying model structure changes (the modelStructureChanged method is invoked) the following are reset to their default values: Comparators by column, current sort order, and whether each column is sortable. The default sort order is natural (the same as the model), and columns are sortable by default.

This was found in API documentation: TableRowSorter

I guess one might say I do not want it to reset to default sort order, but rather keep the last chosen one. Surely there must be a somewhat easy way to stop it from doing that?

Btw Im a noob, please keep that in mind when answering and I hope I made sense in trying to explain the expected behaviour.

My code sofar:

package TestTable;

import java.awt.Color;
import java.awt.Dimension;
import java.util.Random;
import java.util.Vector;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableRowSorter;
import ui.Runner;

public class starter {
    public static void main(String[] args) {
        starter start = new starter();
        Vector<String> columNames = new Vector<>();
        columNames.add("index");
        columNames.add("years");
        columNames.add("weight");
        Vector<Vector<Object>> data = new Vector<Vector<Object>>();
        Vector<Object> dataInData = new Vector<>();
        Random dice = new Random();

        JTable table = new JTable(data, columNames);
        //table.setAutoCreateRowSorter(true);
        MyFrame frame = new MyFrame(table);
        DefaultTableModel model = (DefaultTableModel)table.getModel();
        TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(model);
        //MyRowSorter<DefaultTableModel> sorter = new MyRowSorter<>();
        table.setRowSorter(sorter);
        //DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter();
        //TableModelEvent ev = new TableModelEvent(model);
        sorter.setSortsOnUpdates(true);//<---------------<<<<<

        data.add(new Vector<Object>());
        data.add(new Vector<Object>());
        data.add(new Vector<Object>());

        for(int x = 0; x < 3; x++) {
            dataInData = data.get(x);
            for(int y = 0; y < 5; y++) {
                dataInData.add(dice.nextInt(10));
            }
        }

        for(int z = 0; z < 10; z++) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int x = 0; x < 3; x++) {
                dataInData = data.get(x);
                for(int y = 0; y < dataInData.size(); y++) {
                    dataInData.set(y, dice.nextInt(10));
                }
            }

            model.setDataVector(data, columNames);//<-------------------<<
            //model.fireTableDataChanged();
            //sorter.setSortsOnUpdates(true);
            //sorter.rowsUpdated(0, data.size() -1);
            model.fireTableRowsUpdated(0, model.getRowCount() -1 );
            //model.fireTableDataChanged();
            //model.fireTableChanged(ev);
            //model.fireTableStructureChanged();
            //sorter.rowsUpdated(0, data.size() );
            //sorter.sort();
        }
    }

    public static class MyFrame extends JFrame{
        JTable table;
        Dimension dim = new Dimension(300, 500);
        JScrollPane pane;
        Vector<String> columNames = new Vector<>();
        Vector<Vector<Object>> dataVector;
        Runner thread;
        int[] i = {0, 1, -1};

        public MyFrame(JTable table) {
            pane = new JScrollPane(table);
            table.setFillsViewportHeight(true);
            setSize(300, 500);
            setPreferredSize(dim);
            setMinimumSize(dim);
            setTitle("Failing autoupdate of JTable");
            setBackground(Color.BLACK);
            add(pane);
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            setVisible(true);
        }
    }
}

Solution

  • Let's just step past the thread violation issues for a second.

    Calling setDataVector on the DefaultTableModel triggers a TableStructureChanged, which forces the JTable to, amongst other things, discard/reset the rowSorter, instead, simply update the values in model directly, for example

    for (int x = 0; x < 3; x++) {
        for (int y = 0; y < table.getColumnCount(); y++) {
            model.setValueAt(dice.nextInt(10), x, y);
        }
    }
    

    I tested this using a Swing Timer instead, to ensure that the updates were occurring the EDT properly

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.util.Random;
    import java.util.Vector;
    import javax.swing.JFrame;
    import javax.swing.JScrollPane;
    import javax.swing.JTable;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    import javax.swing.table.DefaultTableModel;
    import javax.swing.table.TableRowSorter;
    
    public class Main {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    new Main();
                }
            });
        }
    
        public Main() {
            Vector<String> columNames = new Vector<>();
            columNames.add("index");
            columNames.add("years");
            columNames.add("weight");
            Vector<Vector<Object>> data = new Vector<Vector<Object>>();
            Random dice = new Random();
    
            JTable table = new JTable(data, columNames);
            MyFrame frame = new MyFrame(table);
            DefaultTableModel model = (DefaultTableModel) table.getModel();
            TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(model);
            table.setRowSorter(sorter);
            sorter.setSortsOnUpdates(true);//<---------------<<<<<
    
            for (int x = 0; x < 3; x++) {
                model.addRow(new Vector());
                for (int y = 0; y < table.getColumnCount(); y++) {
                    model.setValueAt(dice.nextInt(10), x, y);
                }
            }
    
            Timer timer = new Timer(2000, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    for (int x = 0; x < 3; x++) {
                        for (int y = 0; y < table.getColumnCount(); y++) {
                            model.setValueAt(dice.nextInt(10), x, y);
                        }
                    }
                }
            });
            timer.start();
        }
    
        public static class MyFrame extends JFrame {
    
            JTable table;
            Dimension dim = new Dimension(300, 500);
            JScrollPane pane;
            Vector<String> columNames = new Vector<>();
            Vector<Vector<Object>> dataVector;
            int[] i = {0, 1, -1};
    
            public MyFrame(JTable table) {
                pane = new JScrollPane(table);
                table.setFillsViewportHeight(true);
                setSize(300, 500);
                setPreferredSize(dim);
                setMinimumSize(dim);
                setTitle("Failing autoupdate of JTable");
                setBackground(Color.BLACK);
                add(pane);
                setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                setVisible(true);
            }
        }
    }
    

    I chose to change the data with DefaultTableModel.setDataVector(), instead of row by row which seems bulky if lots of rows change

    Okay, that seems reasonable. The only solution I seem to be able to get to work for this is to replace the RowSorter, but use the sortKeys from the old RowSorter

    Just using the same instance of RowSorter didn't work for me

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.util.Random;
    import java.util.Vector;
    import javax.swing.JFrame;
    import javax.swing.JScrollPane;
    import javax.swing.JTable;
    import javax.swing.RowSorter;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    import javax.swing.table.DefaultTableModel;
    import javax.swing.table.TableModel;
    import javax.swing.table.TableRowSorter;
    
    public class Main {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    new Main();
                }
            });
        }
    
        Random dice = new Random();
    
        public Main() {
            Vector<String> columNames = new Vector<>();
            columNames.add("index");
            columNames.add("years");
            columNames.add("weight");
            Vector<Vector<Object>> data = new Vector<Vector<Object>>();
    
            JTable table = new JTable();
            makeModel(table, columNames);
            MyFrame frame = new MyFrame(table);
    
            Timer timer = new Timer(2000, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    makeModel(table, columNames);
                }
            });
            timer.start();
        }
    
        protected void makeModel(JTable table, Vector columnNames) {
            DefaultTableModel model = new DefaultTableModel(columnNames, 0);
    
            for (int x = 0; x < 3; x++) {
                Vector row = new Vector();
                for (int y = 0; y < model.getColumnCount(); y++) {
                    row.add(dice.nextInt(10));
                }
                model.addRow(row);
            }
            TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(model);
            RowSorter<? extends TableModel> oldSorter = table.getRowSorter();
            if (oldSorter != null) {
                sorter.setSortKeys(oldSorter.getSortKeys());
            }
    
            table.setModel(model);
            table.setRowSorter(sorter);
        }
    
        public static class MyFrame extends JFrame {
    
            JTable table;
            Dimension dim = new Dimension(300, 500);
            JScrollPane pane;
            Vector<String> columNames = new Vector<>();
            Vector<Vector<Object>> dataVector;
            int[] i = {0, 1, -1};
    
            public MyFrame(JTable table) {
                pane = new JScrollPane(table);
                table.setFillsViewportHeight(true);
                setSize(300, 500);
                setPreferredSize(dim);
                setMinimumSize(dim);
                setTitle("Failing autoupdate of JTable");
                setBackground(Color.BLACK);
                add(pane);
                setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                setVisible(true);
            }
        }
    }
    

    Just note, that this kind of update will also remove the column/row selection as well

    I chose to change the data with DefaultTableModel.setDataVector(), instead of row by row which seems bulky if lots of rows change

    Another choice would be not to use a DefaultTableModel, but implement your own using AbstractTableModel, where you do a "batch" update to the underlying model data and then trigger a tableDataChanged event. You'd have to be careful to make sure you don't modify the data which might be requested by the JTable until you've completed the batch update