Row filter does not work properly on cell update events

Working in the example with the general model table, I realized that if we attach a row filter to the table row sorter, this filter does not have any effect on the cell update events. According to the RowSorter API :

Specific RowSorter implementations must reference a model such as TableModel or ListModel . View classes such as JTable and JList will also have a model reference. To avoid dependency ordering, the RowSorter implementation should not set the listener on the model. Instead, the view class will call RowSorter when the model changes. For example, if a row is updated in a TableModel JTable calls rowsUpdated . When a model changes, the view can call in any of the following ways: modelStructureChanged , allRowsChanged , rowsInserted , rowsDeleted and rowsUpdated .

So, I understand this paragraph, updating a cell is a special case of updating a row, and as such rowsUpdated should be called and the rowsUpdated should be filtered accordingly.

To illustrate what I'm saying, consider this simple filter:

 private void applyFilter() { DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter(); sorter.setRowFilter(new RowFilter() { @Override public boolean include(RowFilter.Entry entry) { Boolean value = (Boolean)entry.getValue(2); return value == null || value; } }); } 

Here it is expected that the third column will be Boolean and entry (row) if the cell value is either null or true . If I edit a cell placed in the third column and set its value to false , then I expect this row to simply โ€œdisappearโ€ from the view. However, for this I need to install a new filter again, because it does not work โ€œautomaticallyโ€.

TableModelListener to the model as follows, I can see the update event by changes in the cell:

 model.addTableModelListener(new TableModelListener() { @Override public void tableChanged(TableModelEvent e) { if (e.getType() == TableModelEvent.UPDATE) { int row = e.getLastRow(); int column = e.getColumn(); Object value = ((TableModel)e.getSource()).getValueAt(row, column); String text = String.format("Update event. Row: %1s Column: %2s Value: %3s", row, column, value); System.out.println(text); } } }); 

As I said, if I reset the filter using this TableModelListener , then it works as expected:

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

Question: - a problem with an error / implementation? Or do I not understand the API?

The following is a complete MCVE problem.

 import java.awt.BorderLayout; import javax.swing.BorderFactory; import javax.swing.DefaultRowSorter; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.RowFilter; import javax.swing.SwingUtilities; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; public class Demo { private JTable table; private void createAndShowGUI() { DefaultTableModel model = new DefaultTableModel(5, 3) { @Override public boolean isCellEditable(int row, int column) { return column == 2; } @Override public Class<?> getColumnClass(int columnIndex) { return columnIndex == 2 ? Boolean.class : super.getColumnClass(columnIndex); } }; model.addTableModelListener(new TableModelListener() { @Override public void tableChanged(TableModelEvent e) { if (e.getType() == TableModelEvent.UPDATE) { int row = e.getLastRow(); int column = e.getColumn(); Object value = ((TableModel)e.getSource()).getValueAt(row, column); String text = String.format("Update event. Row: %1s Column: %2s Value: %3s", row, column, value); System.out.println(text); // applyFilter(); un-comment this line to make it work } } }); table = new JTable(model); table.setAutoCreateRowSorter(true); applyFilter(); JPanel content = new JPanel(new BorderLayout()); content.setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); content.add(new JScrollPane(table)); JFrame frame = new JFrame("Demo"); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.add(content); frame.pack(); frame.setLocationByPlatform(true); frame.setVisible(true); } private void applyFilter() { DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter(); sorter.setRowFilter(new RowFilter() { @Override public boolean include(RowFilter.Entry entry) { Boolean value = (Boolean)entry.getValue(2); return value == null || value; } }); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { new Demo().createAndShowGUI(); } }); } } 
+6
source share
1 answer

Well, after some research and reading the API, bug reports, and the Oracle forum, I found some interesting things.

1. Set the DefaultRowSorter sortsOnUpdate property to true

The first thing I found was to set the sortsOnUpdate property to true to enable the notification chain when rowsUpdated (...) . Otherwise, no RowSorterEvent will be launched, and the view (our JTable) will not know that something has happened and will not repaint accordingly. So make this small change:

 DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter(); sorter.setRowFilter(new RowFilter() { @Override public boolean include(RowFilter.Entry entry) { Boolean value = (Boolean)entry.getValue(2); return value == null || value; } }); sorter.setSortsOnUpdates(true); 

We do not have to reapply the filter to update the table model. But...

2. There is an error in the JTable component processing the RowSorterEvent notification

While the JTable implements the RowSorterListener interface, subscribes to the row sorter as a listener and as a RowSorterEvents process. there error repaints the table. Odd behavior is well described in these posts:

In a nutshell:

When the RowSorterEvent.TYPE.SORTED event RowSorterEvent.TYPE.SORTED handled by JTable, it displays only the area associated with the overlays, but not the rest of the table that remains as it was. Let's say we edit the first line and now we need to filter it. Then the rest of the rows should be shifted one row up, but it turns out they are not: only the first row will be correctly repainted to show the second row, but the rest of the table remains the same. This is actually a mistake, because in this particular case the whole table needs to be repainted. See basic error. # 6791934

As a workaround, we could either attach a new RowSorterListener to RowSorter , or override JTable sorterChanged (...) as follows to force the entire redraw on our table (IMHO the second method is preferable).

 DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter(); ... sorter.addRowSorterListener(new RowSorterListener() { @Override public void sorterChanged(RowSorterEvent e) { if (e.getType() == RowSorterEvent.Type.SORTED) { // We need to call both revalidate() and repaint() table.revalidate(); table.repaint(); } } }); 

or

 JTable table = new JTable(tableModel) { @Override public void sorterChanged(RowSorterEvent e) { super.sorterChanged(e); if (e.getType() == RowSorterEvent.Type.SORTED) { resizeAndRepaint(); // this protected method calls both revalidate() and repaint() } } }; 

3. The JXTable component has a workaround for this error.

The JXTable component, which is part of the SwingX library and extends from JTable, does not have this problem, because the SwingLabs team rewritten sorterChanged(...) mthod as follows to crack this error:

 //----> start hack around core issue 6791934: // table not updated correctly after updating model // while having a sorter with filter. /** * Overridden to hack around core bug * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6791934 * */ @Override public void sorterChanged(RowSorterEvent e) { super.sorterChanged(e); postprocessSorterChanged(e); } /** flag to indicate if forced revalidate is needed. */ protected boolean forceRevalidate; /** flag to indicate if a sortOrderChanged has happened between pre- and postProcessModelChange. */ protected boolean filteredRowCountChanged; /** * Hack around core issue 6791934: sets flags to force revalidate if appropriate. * Called before processing the event. * @param e the TableModelEvent received from the model */ protected void preprocessModelChange(TableModelEvent e) { forceRevalidate = getSortsOnUpdates() && getRowFilter() != null && isUpdate(e) ; } /** * Hack around core issue 6791934: forces a revalidate if appropriate and resets * internal flags. * Called after processing the event. * @param e the TableModelEvent received from the model */ protected void postprocessModelChange(TableModelEvent e) { if (forceRevalidate && filteredRowCountChanged) { resizeAndRepaint(); } filteredRowCountChanged = false; forceRevalidate = false; } /** * Hack around core issue 6791934: sets the sorter changed flag if appropriate. * Called after processing the event. * @param e the sorter event received from the sorter */ protected void postprocessSorterChanged(RowSorterEvent e) { filteredRowCountChanged = false; if (forceRevalidate && e.getType() == RowSorterEvent.Type.SORTED) { filteredRowCountChanged = e.getPreviousRowCount() != getRowCount(); } } //----> end hack around core issue 6791934: 

So, this is another reason (if some are missing) to use SwingX .

+4
source

Source: https://habr.com/ru/post/976830/


All Articles