Working in a shared table model example I realized that if we attach a row filter to a table's row sorter this filter doesn't have any effect on cell update events. According to RowSorter API:
Concrete implementations of
RowSorter
need to reference a model such asTableModel
orListModel
. The view classes, such asJTable
andJList
, will also have a reference to the model. To avoid ordering dependencies,RowSorter
implementations should not install a listener on the model. Instead the view class will call into theRowSorter
when the model changes. For example, if a row is updated in aTableModel
JTable
invokesrowsUpdated
. When the model changes, the view may call into any of the following methods:modelStructureChanged
,allRowsChanged
,rowsInserted
,rowsDeleted
androwsUpdated
.
So as I understand this paragraph, a cell update is a particular case of row update and as such rowsUpdated
should be called and row filtered accordingly.
To illustrate what I'm saying, please 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 the third column is expected to be a Boolean
and entry
(row) has to be included if the cell value is either null
or true
. If I edit a cell placed at third column and set its value to false
then I'd expect this row just "disappear" from the view. However, to accomplish this I have to set a new filter again because it doesn't seem to work "automatically".
Attaching a TableModelListener
to the model as follows, I can see the update event on cell edits:
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've 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: is this a bug/implementation issue? Or I'm misunderstanding the API?
Here is a complete MCVE illustrating the 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();
}
});
}
}
After doing some research and reading the API, bugs reports and Oracle forum I've found some interesting things.
The first thing I found is we have to set sortsOnUpdate
property to true
in order to enable the notification chain when rowsUpdated(...) is called. Otherwise no RowSorterEvent will be fired and the view (our JTable) won't be aware that something happened and won't repaint accordingly. So making this little 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 won't have to re-apply the filter on a table model update. But...
While JTable implements RowSorterListener
interface, subscribes itself to the row sorter as a listener and process RowSorterEvents
. there's a bug repainting the table. The odd behavior is well described in these posts:
In a nutshell:
When a RowSorterEvent.TYPE.SORTED
event is processed by JTable, it repaints only the area related to the involded rows but not the rest of the table, which remains as it was. Let's say we edit the first row and it should be filtered now. Then the rest of the rows should be shifted up one row to top 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 still the same. This is in fact a bug because in this particular case the whole table needs to be repainted. See core bug # 6791934
As a workaround we could either attach a new RowSorterListener
to the RowSorter
or override JTable's sorterChanged(...) as follows in order to force a whole repaint on our table (IMHO the second method is preferred).
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()
}
}
};
The JXTable component that is part of SwingX library and extends from JTable doesn't have this problem because SwingLabs team has overwrote sorterChanged(...)
mthod as follows to hack around this bug:
//----> 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
* https://bugs.java.com/bugdatabase/view_bug?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 one more reason (if some missing) to use SwingX
.