The right approach to update complex JTables, Tabl

2019-07-20 03:01发布

问题:

My GUI shows the vehicles in my park, and vehicles that I want to set availables in two different VehicleTables (classes that extend JTable). For availables I intend that these vehicles can be observed from an agent (third-part software). Both the tables show the descriptions of Vehicles in the rows...for this I have created VehicleTableModel and Vehicle classes. The Vehicle class is an abstract class and his subclasses are: Car, Truck, Trailer, etc. .

You can see a snapshot of my software:

My problems are these: In my current implementation I don't think of manage really good the updates of the rows. You can see in VehicleTableModel (fire...() methods) and in ShipperAgentGUI (coordinators and listeners). I think I have partially resolved this problem with the use of the Coordinator inner class for the updates between tables, but I don't know how optimize these. For example in case of delete or update of a row I make xxxTable.repaint(); ... the WHOLE table...

...another way?

ShipperAgentGUI.java

public class ShipperAgentGUI extends JFrame implements ActionListener {

    // Graphics variables..
    // bla bla...

    // Headers, TableModels, JTables for the tables
    private COLUMNS[] parkModelHeader = {COLUMNS.IMAGE_COLUMN, COLUMNS.TARGA_COLUMN,
        COLUMNS.CAR_TYPE_COLUMN, COLUMNS.MARCA_COLUMN, COLUMNS.STATE_COLUMN, COLUMNS.PTT_COLUMN };
    private COLUMNS[] availablesModelHeader = {COLUMNS.IMAGE_COLUMN, COLUMNS.TARGA_COLUMN,
        COLUMNS.CAR_TYPE_COLUMN, COLUMNS.MARCA_COLUMN };

    private VehicleTableModel parkModel = new VehicleTableModel(parkModelHeader);
    private VehicleTableModel availablesModel = new VehicleTableModel(availablesModelHeader);

    private VehicleTable parkTable;
    private VehicleTable availablesTable;

    // My third-part software, a JADE agent:
    protected ShipperAgent shipperAgent;


    // --------------------------------------------------------------------------

    // CONSTRUCTOR

    ShipperAgentGUI(ShipperAgent agent) {

        shipperAgent = agent; // valorizes the agent

        setTitle("Shipper Agent: "+agent.getLocalName()+" GUI");

        // graphic bla bla...

        // Park Table and Available Table:
        parkTable = new VehicleTable(parkModel);
            // bla bla...
        availablesTable = new VehicleTable(availablesModel);
            // bla bla...

        // JButtons: add/remove vehicle in Park Table and Available Table
        btnPM_plus = new JButton();
            btnPM_plus.setToolTipText("Add vehicle");
            btnPM_plus.setIcon(...);
            btnPM_plus.setActionCommand("+park");
            btnPM_plus.addActionListener(this);

        // similar things for other three buttons:
        // remove from parkTable, add and remove from availablesTable

        //bla bla...

        // Data from agent:
        Vector<Vehicle> veicoli = shipperAgent.getVehicles();
        Iterator<Vehicle> I = veicoli.iterator();
        while (I.hasNext()){
            addVehicle(parkCoordinator, I.next());
        }

        showGui();
    }



    ///////////////////////////////////////////////////////////////////////
    // Methods:

    public void showGui() {
        // bla bla
    }


    //////////////////////////////////////////////
    // actionPerformed method

    @Override
    public void actionPerformed(ActionEvent e) {
        switch (e.getActionCommand()) {
        case "+park": {
            new InsertVehicleJDialog(this, parkCoordinator);
        } break;

        case "-park": {
            int selectedRow = parkTable.getSelectedRow();
            if (selectedRow != -1)
                removeVehicle(parkCoordinator, selectedRow);
        } break;

        case "+available": {
            int selectedRow = parkTable.getSelectedRow();
            if (selectedRow != -1){
                addVehicle(availablesCoordinator, parkModel.getVehicleAt(selectedRow)); 
            }
        } break;

        case "-available": {
            int selectedRow = availablesTable.getSelectedRow();
            if (selectedRow != -1)
                removeVehicle(availablesCoordinator, selectedRow);
        } break;
        }
    }


    ///////////////////////////////////////
    // Add/Remove vehicle methods:

    void addVehicle(Coordinator coordinator, Vehicle v) {
        coordinator.notifyAndAddRow(v);
    }

    // mhm...
    void removeVehicle(Coordinator coordinator, Vehicle v) {
        int row = coordinator.indexOf(v);
        if (row!=-1)
            coordinator.notifyAndDeleteRow(row);
    }

    void removeVehicle(Coordinator coordinator, int index) {
        coordinator.notifyAndDeleteRow(index);
    }


    // on dispose, delete the agent
    public void dispose() {
        super.dispose();
        shipperAgent.doDelete(); 
    }




    ///////////////////////////////////////
    // INNER CLASS COORDINATOR:

    protected abstract class Coordinator {
        private VehicleTableModel tableModel;

        public Coordinator(VehicleTableModel tm) {
            tableModel = tm;
            notifyRowUpdated();
        }

        public abstract void notifyAndAddRow(Vehicle vehicle);
        public abstract void notifyAndDeleteRow(int rowIndex);
        public abstract void notifyRowUpdated();

        public int indexOf(Vehicle v) {
            return tableModel.indexOf(v);
        }

        boolean vehicleExists(Vehicle vehicle){
            int bool = indexOf(vehicle);
            if (bool==-1) return false;
            else return true;
        }
    }


    // Coordinator for parkTable
    Coordinator parkCoordinator = new Coordinator(parkModel) {

        @Override
        public void notifyAndAddRow(final Vehicle vehicle) {
            if (!vehicleExists(vehicle)){ // is this the right control? Or in VehicleTableModel ?
                shipperAgent.newTruck(vehicle.getPlate());

                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        parkModel.addRow(vehicle);
                        if (vehicle.getState().equals(Stato.DISPONIBILE))
                            availablesModel.addRow(vehicle); 
                            // or with availablesCoordinator.notifyAndAddRow(vehicle) ?
                            // or with addVehicle(availablesCoordinator, vehicle) ?
                            // or with a kind of listener on vehicle's state ?
                    }
                });
            }
        }

        @Override
        public void notifyAndDeleteRow(final int rowIndex) {
            final Vehicle v = parkModel.getVehicleAt(rowIndex);

            removeVehicle(availablesCoordinator, v); // Remove also from the "availables"

            shipperAgent.removeTruck(v.getPlate());

            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    parkModel.removeRow(rowIndex);
                }
            });
        }

        @Override
        public void notifyRowUpdated() {
            parkModel.addTableModelListener(new TableModelListener() {
                public void tableChanged(TableModelEvent e) {
                    switch (e.getType()) {
                        case (TableModelEvent.DELETE):
                            parkTable.repaint();
                            break;
                        case (TableModelEvent.UPDATE):
                            int row = e.getLastRow();
                            Vehicle v = parkModel.getVehicleAt(row);
                            if (v.getState().equals(Stato.DISPONIBILE)){
                                addVehicle(availablesCoordinator, v);
                                availablesTable.repaint();
                            } else
                                removeVehicle(availablesCoordinator, v);
                            parkTable.repaint();
                            break;
                    }
                }
            });
        }
    };



    // Coordinator for availablesTable
    Coordinator availablesCoordinator = new Coordinator(availablesModel) {

        @Override
        public void notifyAndAddRow(final Vehicle vehicle) {
            if (!vehicleExists(vehicle)){ // is this the right control? Or in VehicleTableModel ?
                vehicle.setStato(Stato.DISPONIBILE);
                parkTable.repaint();

                shipperAgent.activateTruck(vehicle.getPlate());
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        availablesModel.addRow(vehicle);
                    }
                });
            }
        }

        @Override
        public void notifyAndDeleteRow(final int rowIndex) {
            Vehicle v = availablesModel.getVehicleAt(rowIndex);
            if (v!=null){
                v.setStato(Stato.NON_DISPONIBILE); // mhm
                shipperAgent.deactivateTruck(v.getPlate());

                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        availablesModel.removeRow(rowIndex);
                    }
                });
            }
        }

        @Override
        public void notifyRowUpdated() {
            availablesModel.addTableModelListener(new TableModelListener() {
                public void tableChanged(TableModelEvent e) {
                    switch (e.getType()) {
                    case (TableModelEvent.DELETE):
                        parkTable.repaint();
                        break;
                    case (TableModelEvent.UPDATE):
                        parkTable.repaint();
                        break;
                    }
                }
            });
        }
    };

}

VehicleTableModel.java

public class VehicleTableModel extends AbstractTableModel {

    private ArrayList<Vehicle> vehicles ;
    private COLUMNS[] header;

    // possible column names:
    public enum COLUMNS {
        IMAGE_COLUMN,
        TARGA_COLUMN,
        CAR_TYPE_COLUMN,
        MARCA_COLUMN,
        STATE_COLUMN,
        PTT_COLUMN,
    };

    ///////////////////////////////////////////////////////
    // Constructor:

    public VehicleTableModel(COLUMNS[] headerTable) {
        this.vehicles = new ArrayList<Vehicle>();
        this.header = headerTable;
    }


    ///////////////////////////////////////////////////////
    // obligatory override methods (from AbstractTableModel):

    @Override
    public int getColumnCount() {
        return header.length;
    }

    @Override
    public int getRowCount() {
        return vehicles.size();
    }

    @Override
    public Object getValueAt(int row, int col) {
        Object value = "?";
        Vehicle v = vehicles.get(row);
        if (v!=null) {
            COLUMNS column = header[col];
            switch (column) {
                case IMAGE_COLUMN:
                    value = VehicleUtils.findImageByColumnCarType(v.getType());
                    break;
                case TARGA_COLUMN:
                    value = v.getPlate();
                    break;
                case CAR_TYPE_COLUMN:
                    value = VehicleUtils.findStringByColumnCarType(v.getType());
                    break;
                // other cases... bla bla...
            }
        }
        return value;
    }



    ///////////////////////////////////////////////////////
    // My methods:

    public void addRow(Vehicle vehicle) {
        vehicles.add(vehicle);
        fireTableRowsInserted(0, getRowCount()); // is right?
    }

    /*public boolean removeRow(Vehicle vehicle) {
        boolean flag = vehicles.remove(vehicle);
        fireTableRowsDeleted(0, getRowCount()); // is right?
        return flag;
    }*/

    public void removeRow(int row) {
        vehicles.remove(row);
        fireTableRowsDeleted(row, row); // is right?
    }

    public Vehicle getVehicleAt(int row) {
        return vehicles.get(row);
    }

    public int indexOf(Vehicle v){
        return vehicles.indexOf(v);
    }

    // found the corresponding column index
    public int findColumn(COLUMNS columnName) {
        for (int i=0; i<getColumnCount(); i++)
            if (columnName.equals(header[i])) 
                return i;
        return -1;
    }


    // a value in that column exist in the table?
    private boolean controllIfExist(Object value, int col) {
        boolean bool = false;
        for (int i=0; i<getRowCount();i++){
            if (value.equals(getValueAt(i, col))){
                bool=true;
                break;
            }
        }
        return bool;
    }

    public int getColumnIndex(COLUMNS column){
        for(int i=0;i<header.length;i++){
            if (column.equals(header[i])){
                return i;
            }
        }
        return -1;
    }



    ///////////////////////////////////////////////////////
    // other methods (from AbstractTableModel) to override:


    @Override
    public Class<?> getColumnClass(int col) {
        Class<?> c;
        COLUMNS column = header[col];
        if (column.equals(COLUMNS.IMAGE_COLUMN))
            c = ImageIcon.class;
        else if (column.equals(COLUMNS.STATE_COLUMN))
            c =  JComboBox.class;
        else c = super.getColumnClass(col);
        return c;
    }


    @Override
    public String getColumnName(int col) {
        COLUMNS column = header[col];
        if (column.equals(COLUMNS.IMAGE_COLUMN))
            return " ";
        else if (column.equals(COLUMNS.TARGA_COLUMN))
            return "Targa";
        // others... bla bla...
        return super.getColumnName(col);
    };


    @Override
    public boolean isCellEditable(int row, int col) {
        return true;
    }


    @Override
    public void setValueAt(Object value, int row, int col) {
        Vehicle v = vehicles.get(row);
        boolean flag = false;
        if (v!=null) {
            COLUMNS column = header[col];
            switch (column) {
                case TARGA_COLUMN:
                    if (!v.getPlate().equals(value)){
                        if (!controllIfExist(value, col)){  // mhm...
                            v.setPlate((String) value);
                            flag = true;
                        }
                    }
                    break;
                case MARCA_COLUMN:
                    if (!v.getMark().equals(value)){
                        v.setMark((String) value);
                        flag = true;
                    }
                    break;

                // others ... bla bla...
            }
            // update ONLY if necessary:
            if (flag) fireTableRowsUpdated(row, row); // is right?
        }
    }
}

回答1:

The whole matter starts at TableModel implementation, so let's take a look to it:

public class VehicleTableModel extends AbstractTableModel {

    private ArrayList<Vehicle> vehicles;

    // Most of your code here, didn't examine it closer though

    public void addRow(Vehicle vehicle) {
        int rowIndex = vehicles.size();
        vehicles.add(vehicle);
        fireTableRowsInserted(rowIndex, rowIndex); // just notify last row == vehicles.size() == getRowCount()
    }

    public void removeRow(int row) {
        vehicles.remove(row);
        fireTableRowsDeleted(row, row); // is right? yes, it looks ok.
    }

    @Override
    public void setValueAt(Object value, int row, int col) {
        Vehicle v = vehicles.get(row);
        if (v != null) {
            COLUMNS column = header[col];
            switch (column) {
                case TARGA_COLUMN:...; break;
                case MARCA_COLUMN:...; break;
                // others...
            }
            fireTableCellUpdated(row, column); // this is the appropriate fire method.
        }
    }

    /**
     * Convenience method to notify if a vehicle was updated in 
     * the outside, not through setValueAt(...).
     */
    public void notifyVehicleUpdated(Vehicle vehicle) {
        Vehicle[] elements = (Vehicles[])vehicles.toArray();
        for (int i = 0; i < elements.length; i++) {
            if (elements[i] == vehicle) {
                fireTableRowsUpdated(i, i);
            }
        }
    }

}

Some other hints:

  • Never use repaint() nor updateUI() to refresh table's data. It's table model responsibility to notify the view about the right event.

  • Never use fireTableDataChanged() (as someone suggested) unless the whole table model data has changed. There are appropriate fireXxxx() methods for rows, columns and cells changes.

  • As far as I understand the problem, both tables share the vehicles list and thus you have to keep them in synch. If so, I'm wondering why do you need two different table models? If the only reason is the status available/parked (mutually exclusive) then you can have a single table model shared along two tables and apply different filters accordingly to the vehicle's status. On status field update, both tables will be notified and vehicle will be transferred from one table to another.

Update

Some time ago in a comment to this answer the idea of adding a method such as notifyRowUpdated() to Coordinator abstract class seemed to be appropriated to solve the synchronization matter between both tables.

But now I think the best approach is sharing the same table model along with the two tables and filtering the second table based on vehicle's status: if available (DISPONIBILE) then show it, if not then hide it.

This way on both row update and row delete both tables will be notified and will act accordingly. On cell update we can add a TableModelListener to the model that applies a filter on second table, showing available vehicles and hidding non available ones. Not to mention that Coordinator abstract class will remain simple and keep its original purpose: notify the third-party agent on row updates/deletes.

So please take a look to the code example below (sorry for the extension). Some notes:

  • I have emulated your Vehicle class with a simpler one. Status is defined by available a boolean property.
  • DataObjectTableModel code is available in tablemodel tag wiki and I've used this class to emulate your table model.
  • Because I don't have any Coordinator class I add/remove rows directly on the table model, but you should do that hrough the appropriate coordinator.
  • Don't know why we have to re-apply filters on table cell update events. As far as I understand the table row sorter should be notified and automatically apply filters. However it doesn't work in this way and we have to manually re-apply fitlers. Minor problem though.

Code example

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.util.Arrays;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultRowSorter;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.RowFilter;
import javax.swing.SwingUtilities;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableColumnModel;

public class DemoSharedTableModel {

    private DataObjectTableModel<Vehicle> model;
    private JTable table1, table2;
    private Action addAction, removeAction;

    private void createAndShowGui() {

        String[] columnIdentifiers = new String[] {
            "Plates",
            "Description",
            "Available"
        };

       model = new DataObjectTableModel<Vehicle>(Arrays.asList(columnIdentifiers)) {

            @Override
            public Class<?> getColumnClass(int columnIndex) {
                switch (columnIndex) {
                    case 0:
                    case 1: return String.class;
                    case 2: return Boolean.class;
                }
                return super.getColumnClass(columnIndex);
            }

            @Override
            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return columnIndex == 2;
            }

            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                Vehicle vehicle = getDataObject(rowIndex);
                switch (columnIndex) {
                    case 0 : return vehicle.getPlates();
                    case 1: return vehicle.getDescription();
                    case 2: return vehicle.isAvailable();
                        default: throw new ArrayIndexOutOfBoundsException(columnIndex);
                }
            }

            @Override
            public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
                if (columnIndex == 2) {
                    Vehicle vehicle = getDataObject(rowIndex);
                    vehicle.setAvailable((Boolean)aValue);
                    fireTableCellUpdated(rowIndex, columnIndex);
                } else {
                    throw new UnsupportedOperationException("Unsupported for column " + columnIndex);
                }
            }
        };

        model.addRow(new Vehicle("AAA1", "Car - Peugeot", true));
        model.addRow(new Vehicle("AAA2", "Truck - Volvo", true));
        model.addRow(new Vehicle("AAA3", "Car - Ford", false));
        model.addRow(new Vehicle("AAA4", "Car - Mercedes-Benz", false));
        model.addRow(new Vehicle("AAA5", "Car - Ferrari", true));

        model.addTableModelListener(new TableModelListener() {
            @Override
            public void tableChanged(TableModelEvent e) {
                if (e.getType() == TableModelEvent.UPDATE) {
                    DemoSharedTableModel.this.applyFilterOnSecondTable();
                }
            }
        });

        table1 = new JTable(model);
        table1.setAutoCreateRowSorter(true);
        table1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

        table2 = new JTable(model);
        table2.setAutoCreateRowSorter(true);
        table2.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        // Make third column not visible
        TableColumnModel columnModel = table2.getColumnModel();
        columnModel.removeColumn(columnModel.getColumn(2));

        applyFilterOnSecondTable();

        addAction = new AbstractAction("+") {
            @Override
            public void actionPerformed(ActionEvent e) {
                model.addRow(new Vehicle("new", "default text", true));
            }
        };

        removeAction = new AbstractAction("-") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int viewIndex = table1.getSelectedRow();
                if (viewIndex != -1) {
                    int modelIndex = table1.convertRowIndexToModel(viewIndex);
                    model.deleteRow(modelIndex);
                }
                setEnabled(model.getRowCount() > 0);
            }
        };

        JPanel buttonsPanel = new JPanel();
        buttonsPanel.add(new JButton(addAction));
        buttonsPanel.add(new JButton(removeAction));

        JPanel content = new JPanel(new BorderLayout(8, 8));
        content.add(new JScrollPane(table1), BorderLayout.WEST);
        content.add(buttonsPanel, BorderLayout.CENTER);
        content.add(new JScrollPane(table2), BorderLayout.EAST);

        JFrame frame = new JFrame("Demo");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.add(content);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }

    private void applyFilterOnSecondTable() {
        DefaultRowSorter sorter = (DefaultRowSorter)table2.getRowSorter();
        sorter.setRowFilter(new RowFilter() {
            @Override
            public boolean include(RowFilter.Entry entry) {
                Vehicle vehicle = model.getDataObject((Integer)entry.getIdentifier());
                return vehicle.isAvailable();
            }
        });
    }

    class Vehicle {

        private String plates, description;
        private Boolean available;

        public Vehicle(String plates, String description, Boolean available) {
            this.plates = plates;
            this.description = description;
            this.available = available;
        }

        public String getPlates() {
            return plates;
        }

        public void setPlates(String plates) {
            this.plates = plates;
        }

        public String getDescription() {
            return description;
        }

        public void setDescription(String description) {
            this.description = description;
        }

        public Boolean isAvailable() {
            return available;
        }

        public void setAvailable(Boolean available) {
            this.available = available;
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new DemoSharedTableModel().createAndShowGui();
            }
        });
    }
}

Screenshot

Note that in second table only available vehicles are displayed.