How does JTable RowFilter work?
Asked Answered
T

2

10

I'm try to create a Row filter for a JTable to limit the number of rows displayed in the table.

The RowFilter code is simple. It converts the model row number to the view row number (in case the table is sorted) and then checks if the view row number is less that the number of lines to be displayed in the table:

RowFilter<TableModel, Integer> filter = new RowFilter<TableModel, Integer>()
{
    @Override
    public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry)
    {
        int modelRow = entry.getIdentifier();
        int viewRow = table.convertRowIndexToView(modelRow);

        return viewRow < numberOfRows;
    }

};

The problem is that the model row number is not always converted to a reasonable view row number so too many rows are being included in the filter. To demonstrate run the code below:

1) Select "1" from the combo box and you will get output like:

Change the Filter to: 1
m0 : v0
m1 : v0
m2 : v0
m3 : v0
m4 : v0

This output is telling me that all model rows are being converted to view row 0. Since 0 is less than the filter value of 1, all rows are included in the filter (which is wrong).

So the question here is why is the convertRowIndexToView(modelRow) not working as expected?

2) Now select "2" from the combo box and you will get output like:

Change the Filter to: 2
m0 : v0
m1 : v1
m2 : v2
m3 : v3
m4 : v4

As you can see the model rows are now mapping to the proper view row, so only 2 row are included in the filter which is correct.

3) Now select "3" from the combo box and you will get output like:

Change the Filter to: 3
m0 : v0
m1 : v1
m2 : v-1
m3 : v-1
m4 : v-1

In this case the last 3 model rows are converted to -1, which I assume means the row is not currently visible in the table, which is correct. So in this case all 5 rows are again included in the filter which is incorrect since we only want the first 3.

So the question here is how to reset the filter so all model rows are mapped to the original view row?

I tried to use:

((TableRowSorter) table.getRowSorter()).setRowFilter(null);
((TableRowSorter) table.getRowSorter()).setRowFilter(filter);

to clear the filter, before resetting the filter but this gave the same results as step 1. That is, now all the model rows map to view row 0 (so all 5 rows are still displayed).

Here is the test code:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class FilterSSCCE extends JPanel
{
    private JTable table;

    public FilterSSCCE()
    {
        setLayout( new BorderLayout() );

        JComboBox<Integer> comboBox = new JComboBox<Integer>();
        comboBox.addItem( new Integer(1) );
        comboBox.addItem( new Integer(2) );
        comboBox.addItem( new Integer(3) );
        comboBox.addItem( new Integer(4) );
        comboBox.addItem( new Integer(5) );
        comboBox.setSelectedIndex(4);

        comboBox.addActionListener( new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                //System.out.println( table.convertRowIndexToView(4) );
                Integer value = (Integer)comboBox.getSelectedItem();
                newFilter( value );
                //System.out.println( table.convertRowIndexToView(4) );
            }
        });
        add(comboBox, BorderLayout.NORTH);

        table = new JTable(5, 1);

        for (int i = 0; i < table.getRowCount(); i++)
            table.setValueAt(String.valueOf(i+1), i, 0);

        table.setAutoCreateRowSorter(true);
        JScrollPane scrollPane = new JScrollPane(table);
        add(scrollPane, BorderLayout.CENTER);
    }

    private void newFilter(int numberOfRows)
    {
        System.out.println("Change the Filter to: " + numberOfRows);

        RowFilter<TableModel, Integer> filter = new RowFilter<TableModel, Integer>()
        {
            @Override
            public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry)
            {
                int modelRow = entry.getIdentifier();
                int viewRow = table.convertRowIndexToView(modelRow);

                System.out.println("m" + modelRow + " : v" + viewRow);

                return viewRow < numberOfRows;
            }

        };

        ((TableRowSorter) table.getRowSorter()).setRowFilter(filter);
    }

    private static void createAndShowGUI()
    {
        JPanel panel = new JPanel();

        JFrame frame = new JFrame("FilterSSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new FilterSSCCE());
        frame.setLocationByPlatform( true );
        frame.pack();
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}

Any idea how to create a row filter to display the first "n" rows?

Oh yeah, one more frustrating point. If you uncomment the two System.out.. lines in the actionPeformed() method, when you select 1 from the combo box you will notice that in both cases the model index 4 is converted to view index 4 and these two outputs are sandwiched around the incorrect model to view conversions???

Edit:

Based on MadProgrammers suggestion I tried:

//((TableRowSorter) table.getRowSorter()).setRowFilter(filter);
TableRowSorter sorter = new TableRowSorter();
table.setRowSorter( sorter );
sorter.setRowFilter( filter );
sorter.sort();

Now I get nothing in the table.

Toughminded answered 26/5, 2015 at 1:57 Comment(9)
"Guess" is that it is "some times" working with the previously filtered data set...Outpour
I ended up creating a new TableRowSorter, applying that to the JTable setting the rowFilter to that sorter and then calling sort on the TableRowSorter ... :POutpour
@MadProgrammer, ...previously filtered data set yes and I guess the question is why? I guess most filters are based on the actual data in the TableModel, so this in not a problem as the mapping of the indexes will be redone after the filtering? I ended up creating... - didn't work for me. Guess I did something else wrong?Toughminded
Admittedly, I'm testing with Java 8Outpour
ps TableRowSorter<TableModel> sorter = new TableRowSorter<>(table.getModel()); ;)Outpour
So, after copying the DefaultRowSorter and TableRowSorter code into some test code, I monitored the changes to the modelToView, which is used by the DefaultRowSorter#convertRowIndexToView, it's using the cached data from the previous filter, meaning that the rows the were previously filtered out are -1. It would seem that you're not suppose to be looking at the view when doing the filtering...Outpour
@MadProgrammer, It would seem that you're not suppose to be looking at the view when doing the filtering - that would make sense. Using your suggestion, I came up with a solution to do the filtering and maintain the sort order.Toughminded
Not sure either solution is a good complete solution. So I'll leave the question unanswered. Use the suggestions at your own risk :)Toughminded
For what it's worth, the documentation for DefaultRowSorter.setRowFilter states: “The filter is applied before sorting.”Fula
T
5

Using the tips from @MadProgrammer I came up with the following solution.

Not only does the RowSorter need to be replaced, you also need to keep the sort keys so the sort() method resets the table back to its current sort state:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class FilterSSCCE extends JPanel
{
    private JTable table;

    public FilterSSCCE()
    {
        setLayout( new BorderLayout() );

        JComboBox<Integer> comboBox = new JComboBox<Integer>();
        comboBox.addItem( new Integer(1) );
        comboBox.addItem( new Integer(2) );
        comboBox.addItem( new Integer(3) );
        comboBox.addItem( new Integer(4) );
        comboBox.addItem( new Integer(5) );
        comboBox.setSelectedIndex(4);

        comboBox.addActionListener( new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                Integer value = (Integer)comboBox.getSelectedItem();
                newFilter( value );
            }
        });
        add(comboBox, BorderLayout.NORTH);

        table = new JTable(5, 1);

        for (int i = 0; i < table.getRowCount(); i++)
            table.setValueAt(String.valueOf(i+1), i, 0);

        table.setAutoCreateRowSorter(true);
        JScrollPane scrollPane = new JScrollPane(table);
        add(scrollPane, BorderLayout.CENTER);
    }

    private void newFilter(int numberOfRows)
    {
        RowFilter<TableModel, Integer> filter = new RowFilter<TableModel, Integer>()
        {
            @Override
            public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry)
            {
                int modelRow = entry.getIdentifier();
                int viewRow = table.convertRowIndexToView(modelRow);

                return viewRow < numberOfRows;
            }

        };

        TableRowSorter oldSorter = (TableRowSorter)table.getRowSorter();
        TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
        table.setRowSorter( sorter );
        sorter.setRowFilter( filter );
        sorter.setSortKeys( oldSorter.getSortKeys() );
        sorter.sort();
    }

    private static void createAndShowGUI()
    {
        JPanel panel = new JPanel();

        JFrame frame = new JFrame("FilterSSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new FilterSSCCE());
        frame.setLocationByPlatform( true );
        frame.pack();
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}
Toughminded answered 26/5, 2015 at 4:49 Comment(2)
thanks for that, very interesting, till yet I was sure that there never needer to reset something because this logics works with filter and sorter together (starting with filtering, then is used the sorter)Rossuck
@MadProgrammer, even this solution has a potential problem depending on the exact requirement. If you display two rows (1, 2) and then change the sort to descending you now see (2, 1). In reality you would probably want to see (5, 4), which means even on a sort order change we would need to reset the RowSorter and filter. Getting more and more complicated.Toughminded
O
7

So, after some serious testing and debugging, I copied the code for DefaultRowSorter and TableRowSorter into your FilterSSCCE and added some output monitoring the modelToView field, which is used by DefaultRowSorter#convertRowIndexToView to map between the model and view indicies...

Change the Filter to: 1
createModelToView = [0, 0, 0, 0, 0]
m0 : v0
m1 : v0
m2 : v0
m3 : v0
m4 : v0
initializeFilteredMapping.1 = [0, 1, 2, 3, 4]
initializeFilteredMapping.2 = [0, 1, 2, 3, 4]
Change the Filter to: 5
m0 : v0
m1 : v1
m2 : v2
m3 : v3
m4 : v4
initializeFilteredMapping.1 = [0, 1, 2, 3, 4]
initializeFilteredMapping.2 = [0, 1, 2, 3, 4]
Change the Filter to: 1
m0 : v0
m1 : v1
m2 : v2
m3 : v3
m4 : v4
initializeFilteredMapping.1 = [0, -1, -1, -1, -1]
initializeFilteredMapping.2 = [0, -1, -1, -1, -1]
Change the Filter to: 2
m0 : v0
m1 : v-1
m2 : v-1
m3 : v-1
m4 : v-1
initializeFilteredMapping.1 = [0, 1, 2, 3, 4]
initializeFilteredMapping.2 = [0, 1, 2, 3, 4]

The interesting part is here at end, between Change the Filter to: 1 and Change the Filter to: 2. You can see that initializeFilteredMapping has set the model indices that are out of range to -1, but when we change to Change the Filter to: 2, those same indices are still set, changing the filter has NOT reset them.

This seems to be a design choice to keep the table responsive and they probably never thought some one might try and access the view from within the filter, as you're suppose to be using the model data...

How to get around it...?

You could build a "proxy" TableModel, but that precludes the possibility that the table might be sorted.

You could write a "proxy" TableModel which "knew" about the sorted state of the JTable (probably via the RowSorter) and which could act as the filter to determine the visible row count, but this is crossing into murky water as the model is starting to venture into the world of the view...

Another choice would be to change the way that the setFilter method works and reset the modelToView and viewToModel variables, but they are private, as they should be, okay, we could use the createModelToView, createViewToModel and setModelToViewFromViewToModel methods available in the DefaultRowSorter ... but they are private to...

It would seem just about any useful method which deals with serious modification to these variables are private...story of my life...(get your torches and pitchforks, we're going on a dev-hunt)

Next choice, write it ALL yourself...what a wonderful idea, expect that goes against the basic principles of OO...

A "work around" (and I use the term very, very lightly), would be to use reflection and just call the methods we need...

public class TestRowSorter<M extends TableModel> extends TableRowSorter<M> {

    public TestRowSorter() {
    }

    public TestRowSorter(M model) {
        super(model);
    }

    public Method findMethod(String name, Class... lstTypes) {

        return findMethod(getClass(), name, lstTypes);

    }

    public Method findMethod(Class parent, String name, Class... lstTypes) {

        Method method = null;
        try {
            method = parent.getDeclaredMethod(name, lstTypes);
        } catch (NoSuchMethodException noSuchMethodException) {
            try {
                method = parent.getMethod(name, lstTypes);
            } catch (NoSuchMethodException nsm) {
                if (parent.getSuperclass() != null) {
                    method = findMethod(parent.getSuperclass(), name, lstTypes);
                }
            }
        }

        return method;

    }

    @Override
    public void setRowFilter(RowFilter<? super M, ? super Integer> filter) {

        try {
            Method method = findMethod("createModelToView", int.class);
            method.setAccessible(true);
            method.invoke(this, getModelWrapper().getRowCount());

            method = findMethod("createViewToModel", int.class);
            method.setAccessible(true);
            method.invoke(this, getModelWrapper().getRowCount());

            method = findMethod("setModelToViewFromViewToModel", boolean.class);
            method.setAccessible(true);
            method.invoke(this, true);
        } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException exp) {
            exp.printStackTrace();
        }

        super.setRowFilter(filter);
    }

}

Now, I'm pretty sure you know, as I do, this is a horrible, horrible idea which could break at any time. It's probably also very, very inefficient, as you're resetting the indexes to the bi-directional look up each time.

So, the answer, don't access the view from the filter.

Generally speaking, I tend to replace the RowSorter when ever I replace the RowFilter as it avoids these kind of issues :P

Outpour answered 26/5, 2015 at 5:4 Comment(1)
Thanks for the full analysis (+1). don't access the view from the filter - Seemed like a simple requirement to just limit the number of rows displayed in the view. But if you think about it does it really matter if all the rows are displayed? Maybe a simpler solution would be to highlight the background of the first "n" rows differently?Toughminded
T
5

Using the tips from @MadProgrammer I came up with the following solution.

Not only does the RowSorter need to be replaced, you also need to keep the sort keys so the sort() method resets the table back to its current sort state:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class FilterSSCCE extends JPanel
{
    private JTable table;

    public FilterSSCCE()
    {
        setLayout( new BorderLayout() );

        JComboBox<Integer> comboBox = new JComboBox<Integer>();
        comboBox.addItem( new Integer(1) );
        comboBox.addItem( new Integer(2) );
        comboBox.addItem( new Integer(3) );
        comboBox.addItem( new Integer(4) );
        comboBox.addItem( new Integer(5) );
        comboBox.setSelectedIndex(4);

        comboBox.addActionListener( new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                Integer value = (Integer)comboBox.getSelectedItem();
                newFilter( value );
            }
        });
        add(comboBox, BorderLayout.NORTH);

        table = new JTable(5, 1);

        for (int i = 0; i < table.getRowCount(); i++)
            table.setValueAt(String.valueOf(i+1), i, 0);

        table.setAutoCreateRowSorter(true);
        JScrollPane scrollPane = new JScrollPane(table);
        add(scrollPane, BorderLayout.CENTER);
    }

    private void newFilter(int numberOfRows)
    {
        RowFilter<TableModel, Integer> filter = new RowFilter<TableModel, Integer>()
        {
            @Override
            public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry)
            {
                int modelRow = entry.getIdentifier();
                int viewRow = table.convertRowIndexToView(modelRow);

                return viewRow < numberOfRows;
            }

        };

        TableRowSorter oldSorter = (TableRowSorter)table.getRowSorter();
        TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
        table.setRowSorter( sorter );
        sorter.setRowFilter( filter );
        sorter.setSortKeys( oldSorter.getSortKeys() );
        sorter.sort();
    }

    private static void createAndShowGUI()
    {
        JPanel panel = new JPanel();

        JFrame frame = new JFrame("FilterSSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new FilterSSCCE());
        frame.setLocationByPlatform( true );
        frame.pack();
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}
Toughminded answered 26/5, 2015 at 4:49 Comment(2)
thanks for that, very interesting, till yet I was sure that there never needer to reset something because this logics works with filter and sorter together (starting with filtering, then is used the sorter)Rossuck
@MadProgrammer, even this solution has a potential problem depending on the exact requirement. If you display two rows (1, 2) and then change the sort to descending you now see (2, 1). In reality you would probably want to see (5, 4), which means even on a sort order change we would need to reset the RowSorter and filter. Getting more and more complicated.Toughminded

© 2022 - 2024 — McMap. All rights reserved.