0

I m trying to implement a Paginated TableView that allows sorting by all items in JavaFX. I implemented the paginated tableview from here: https://stackoverflow.com/a/25424208/12181863. provided by jewelsea and tim buthe.

I was thinking that because the table view is only accessing a sublist of items, i wanted to extend the sorting from the table columns to the full list based on what i understand on the section about sorting on the Java Docs: https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/TableView.html#setItems-javafx.collections.ObservableList-

 // bind the sortedList comparator to the TableView comparator
 //i m guessing it extends the sorting from the table to the actual list?
 sortedList.comparatorProperty().bind(tableView.comparatorProperty());

and then refresh the tableview for the same sublist indexes (which should now be sorted since the whole list is sorted).

Basically, I want to use the table column comparator to sort the full list, and then "refresh" the tableview using the new sorted list. Is this feasible? Or is there a simpler way to go about this?

I also referred to other reference material such as : https://incepttechnologies.blogspot.com/p/javafx-tableview-with-pagination-and.html but i found it hard to understand since everything was all over the place with vague explanation.

A quick extract of the core components in my TouchDisplayEmulatorController class

import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Pagination;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.List;

public class TouchDisplayEmulatorController extends Application {
    public TableView sensorsTable;
    public List<Sensor> sensors;
    public int rowsPerPage = 14;
    public GridPane grids = new GridPane();
    public long timenow;

    public void start(final Stage stage) throws Exception {
        grids = new GridPane();
        setGridPane();

        Scene scene = new Scene(grids, 1024, 768);
        stage.setScene(scene);
        stage.setTitle("Table pager");
        stage.show();
    }

    //public static void main(String[] args) throws Exception {
      //  launch(args);
    //}

    public void setGridPane(){
        processSensors();
        sensorsGrid();
    }

    public void sensorsGrid(){
        buildTable();
        int numOfPages = 1;
        if (sensors.size() % rowsPerPage == 0) {
            numOfPages = sensors.size() / rowsPerPage;
        } else if (sensors.size() > rowsPerPage) {
            numOfPages = sensors.size() / rowsPerPage + 1;
        }
        Pagination pagination = new Pagination((numOfPages), 0);
        pagination.setPageFactory(this::createPage);
        pagination.setMaxPageIndicatorCount(numOfPages);
        grids.add(pagination, 0, 0);
    }

    private Node createPage(int pageIndex) {
        int fromIndex = pageIndex * rowsPerPage;
        int toIndex = Math.min(fromIndex + rowsPerPage, sensors.size());
        sensorsTable.setItems(FXCollections.observableArrayList(sensors.subList(fromIndex, toIndex)));

        return new BorderPane(sensorsTable);
    }

    public void processSensors(){
        sensors = new ArrayList<>();
//        long timenow = OffsetDateTime.now(ZoneOffset.UTC).toInstant().toEpochMilli()/1000;
//        StringTokenizer hildetoken = new StringTokenizer(msg);

        for (int i=0; i<20; i++) {
            sensors.add(new Sensor(String.valueOf(i), "rid-"+i, "sid-"+i, "0", "0", "no condition"));
        }
    }


    public void buildTable() {
        sensorsTable = new TableView();
        TableColumn<Sensor, String> userid = new TableColumn<>("userid");
        userid.setCellValueFactory(param -> param.getValue().userid);
        userid.setPrefWidth(100);
        TableColumn<Sensor, String> resourceid = new TableColumn<>("resourceid");
        resourceid.setCellValueFactory(param -> param.getValue().resourceid);
        resourceid.setPrefWidth(100);
        TableColumn<Sensor, String> column1 = new TableColumn<>("sid");
        column1.setCellValueFactory(param -> param.getValue().sid);
        column1.setPrefWidth(100);
        TableColumn<Sensor, String> column2 = new TableColumn<>("timestamp");
        column2.setCellValueFactory(param -> param.getValue().timestamp);
        column2.setPrefWidth(100);
        TableColumn<Sensor, String> column3 = new TableColumn<>("reading");
        column3.setCellValueFactory(param -> param.getValue().reading);
        column3.setPrefWidth(100);
        TableColumn<Sensor, String> column4 = new TableColumn<>("last contacted");
        column4.setCellFactory(new Callback<TableColumn<Sensor, String>, TableCell<Sensor, String>>() {
            @Override
            public TableCell<Sensor, String> call(TableColumn<Sensor, String> sensorStringTableColumn) {
                return new TableCell<Sensor, String>() {
                    public void updateItem(String item, boolean empty) {
                        super.updateItem(item, empty);
                        if (!isEmpty()) {
                            this.setTextFill(Color.WHITE);
                            if (item.contains("@")) {
                                this.setTextFill(Color.BLUEVIOLET);
                            } else if (item.equals("> 8 hour ago")) {
                                this.setStyle("-fx-background-color: red;");
                            } else if (item.equals("< 8 hour ago")) {
                                this.setStyle("-fx-background-color: orange;");
                                //this.setTextFill(Color.ORANGE);
                            } else if (item.equals("< 4 hour ago")) {
                                this.setStyle("-fx-background-color: yellow;");
                                this.setTextFill(Color.BLACK);
                            } else if (item.equals("< 1 hour ago")) {
                                this.setStyle("-fx-background-color: green;");
                                //this.setTextFill(Color.GREEN);
                            }
                            setText(item);
                        }
                    }
                };
            }
        });
        column4.setCellValueFactory(param -> param.getValue().condition);
        column4.setPrefWidth(100);
        sensorsTable.getColumns().addAll(userid, resourceid, column1, column2, column3, column4);
    }
}
class Sensor {
    public SimpleStringProperty userid;
    public SimpleStringProperty resourceid;
    public SimpleStringProperty sid;
    public SimpleStringProperty timestamp;
    public SimpleStringProperty reading;
    public SimpleStringProperty condition;


    public Sensor(String userid, String resourceid, String sid, String timestamp, String reading, String condition){
        this.userid = new SimpleStringProperty(userid);
        this.resourceid = new SimpleStringProperty(resourceid);
        this.sid = new SimpleStringProperty(sid);
        this.timestamp = new SimpleStringProperty(timestamp);
        this.reading = new SimpleStringProperty(reading);
        this.condition = new SimpleStringProperty(condition);
        //we can use empty string or condition 3 here
    }

    public Sensor(String sid, String timestamp, String reading, String condition){
        this.userid = new SimpleStringProperty("-1");
        this.resourceid = new SimpleStringProperty("-1");
        this.sid = new SimpleStringProperty(sid);
        this.timestamp= new SimpleStringProperty(timestamp);
        this.reading= new SimpleStringProperty(reading);
        this.condition = new SimpleStringProperty(condition);
    }

    public String getUserid() { return this.userid.toString(); }
    public String getResourceid() { return this.resourceid.toString(); }
    public String getSid() { return this.sid.toString(); }
    public String getTimestamp() { return this.timestamp.toString(); }
    public String getReading() { return this.reading.toString(); }
    public String getCondition() { return this.condition.toString(); }
    public String toString() { return "userid: "+getUserid()+" resourceid: "+getResourceid()+" sid: "+getSid()+
            "\ntimestamp: "+getTimestamp()+" reading: "+getReading()+" condition: "+getCondition();}
}

separate class:

public class tester {
    public static void main(String[] args) {
        Application.launch(TouchDisplayEmulatorController.class, args);
    }
}
  • 1
    [mcve] please .. – kleopatra Jul 29 '21 at 09:42
  • It's not clear to me what your problem is. Do you want to click on a column header in the `TableView` and have the `TableView` contents sorted according to the data in that column? And you want that sort to apply to all the pages of the `Pagination`? Maybe you can [edit] your question and post some screen captures illustrating your desired result? And maybe also post some screen captures of what you are getting now? – Abra Jul 31 '21 at 06:18
  • Yeah, i want the sort to apply to all pages of the pagination. ie if column a is sorted in asc, the sort applies to the whole list, so the whole list is sorted in asc based on column a which then "changes" the order the page items are displayed – experiment unit 1998X Jul 31 '21 at 06:31
  • What I am getting currently is that the sort only applies to the current page. Because the TableView is only displaying a sublist of the full list. – experiment unit 1998X Jul 31 '21 at 06:34

2 Answers2

1

Pagination of a TableView is not directly supported, so we have to do it ourselves. Note that the solutions referenced in the question pre-date the (re-introduction of Sorted-/FilteredList)

Nowadays, the basic approach is to use a FilteredList that contains only the rows which are on the current page. This filteredList must be the value of the table's itemsProperty. To also allow sorting, we need to wrap the original data into a SortedList and bind its comparator to the comparator provided by the table. Combining all:

items = observableArrayList(... //my data);
sortedList = new SortedList(items);
filteredList = new FilteredList(sortedList);
table.setItems(filteredList);
sortedList.comparatorProperty().bind(table.comparatorProperty());

Looks good, doesn't it? Unfortunately, nothing happens when clicking onto a column header. The reason:

  • the collaborator that's responsible for the sort is the sortPolicy
  • the default policy checks whether the table's items is a sorted list: if so (and its comparator is bound to the table's), sorting is left to that list, otherwise it falls back to FXCollections.sort(items, ...)
  • collections.sort fails to do anything because a filtered list is unmodifiable

In pseudo code:

if (items instanceof SortedList) {
    return sortedList.getComparator().isBoundTo(table.getComparator());   
}
try {
    FXCollections.sort(items);
    // sorting succeeded
    return true;
} catch (Exception ex) {
    // sorting failed
    return false;
}

The way out is to implement a custom sort policy: instead of only checking the table's items for being a sortedList, it walks up the chain of transformationList (if available) sources until it finds a sorted (or not):

ObservableList<?> lookup = items;
while (lookup instanceof TransformationList) {
    if (lookup instanceof SortedList) {
        items = lookup;
        break;
    } else {
        lookup = ((TransformationList<?, ?>) lookup).getSource();
    }
}
// ... same as original policy

Now we have the sorting (of the complete list) ready - next question is what should happen to the paged view after sorting. Options:

  • keep the page constant and updated the filter
  • keep any of the current items visible and update the page

Both require to trigger the update when the sort state of the list changes, which to implement depends on UX guidelines.

A runnable example:

public class TableWithPaginationSO extends Application {

    public static <T> Callback<TableView<T>, Boolean> createSortPolicy(TableView<T> table) {
        // c&p of DEFAULT_SORT_POLICY except adding search up a chain
        // of transformation lists until we find a sortedList
        return new Callback<TableView<T>, Boolean>() {

            @Override
            public Boolean call(TableView<T> table) {
                try {
                    ObservableList<?> itemsList = table.getItems();
                    
                    // walk up the source lists to find the first sorted
                    ObservableList<?> lookup = itemsList;
                    while (lookup instanceof TransformationList) {
                        if (lookup instanceof SortedList) {
                            itemsList = lookup;
                            break;
                        } else {
                            lookup = ((TransformationList<?, ?>) lookup).getSource();
                        }
                    }

                    if (itemsList instanceof SortedList) {
                        SortedList<?> sortedList = (SortedList<?>) itemsList;
                        boolean comparatorsBound = sortedList.comparatorProperty()
                                .isEqualTo(table.comparatorProperty()).get();

                        return comparatorsBound;
                    } else {
                        if (itemsList == null || itemsList.isEmpty()) {
                            // sorting is not supported on null or empty lists
                            return true;
                        }

                        Comparator comparator = table.getComparator();
                        if (comparator == null) {
                            return true;
                        }

                        // otherwise we attempt to do a manual sort, and if successful
                        // we return true
                        FXCollections.sort(itemsList, comparator);
                        return true;
                    }
                } catch (UnsupportedOperationException e) {
                    return false;
                }
            };

        };
    }

    private Parent createContent() {
        initData();
        // wrap sorted list around data
        sorted = new SortedList<>(data);
        // wrap filtered list around sorted
        filtered = new FilteredList<>(sorted);
        // use filtered as table's items
        table = new TableView<>(filtered);
        addColumns();
        page = new BorderPane(table);

        // install custom sort policy
        table.setSortPolicy(createSortPolicy(table));
        // bind sorted comparator to table's
        sorted.comparatorProperty().bind(table.comparatorProperty());

        pagination = new Pagination(rowsPerPage, 0);
        pagination.setPageCount(sorted.size() / rowsPerPage);;
        pagination.setPageFactory(this::createPage);

        sorted.addListener((ListChangeListener<Locale>) c -> {
            // update page after changes to list 
            updatePage(true);
        });
        return pagination;
    }

    private Node createPage(int pageIndex) {
        updatePredicate(pageIndex);
        return page;
    }

    /**
     * Update the filter to show the current page.
     */
    private void updatePredicate(int pageIndex) {
        int first = rowsPerPage * pageIndex;
        int last = Math.min(first + rowsPerPage, sorted.size());
        Predicate<Locale> predicate = loc -> {
            int index = sorted.indexOf(loc);
            return index >= first && index < last;
        };
        filtered.setPredicate(predicate);
        // keep reference to first on page
        firstOnPage = filtered.get(0);
    }

    /** 
     * Update the page after changes to the list. 
     */
    private void updatePage(boolean keepItemVisible) {
        if (keepItemVisible) {
            int sortedIndex = sorted.indexOf(firstOnPage);
            int pageIndex = sortedIndex >= 0 ? sortedIndex / rowsPerPage : 0;
            pagination.setCurrentPageIndex(pageIndex);
        } else {
            updatePredicate(pagination.getCurrentPageIndex());
        }
    }

    private void addColumns() {
        TableColumn<Locale, String> name = new TableColumn<>("Name");
        name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
        TableColumn<Locale, String> country = new TableColumn<>("Country");
        country.setCellValueFactory(new PropertyValueFactory<>("displayCountry"));
        table.getColumns().addAll(name, country);
    }

    private void initData() {
        Locale[] availableLocales = Locale.getAvailableLocales();
        data = observableArrayList(
                Arrays.stream(availableLocales)
                .filter(e -> e.getDisplayName().length() > 0)
                .limit(120)
                .collect(toList())
                );
    }

    private TableView<Locale> table;
    private Pagination pagination;
    private BorderPane page;
    private ObservableList<Locale> data;
    private FilteredList<Locale> filtered;
    private SortedList<Locale> sorted;
    private Locale firstOnPage;
    private int rowsPerPage = 15;

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
0

First off, it's not clear that Pagination is the correct control for this, although the UI for page selection is really nice. Your problems might stem from the fact that you're putting the TableView into a new BorderPane each time, or it might come from the fact that you're using TableView.setItems().

An approach that works is to use FilteredList to handle the pagination, and just keep the TableView as a static element in the layout, not in the dynamic graphic area of the Pagination. To satisfy the need to have the Pagination do something, a Text with the page number was created.

A new property was added to Sensor - ordinalNumber. This is used to control the filter for the paging. The filter will dynamically change to select only those Sensor's with an ordinalNumber in a particular range. The range is controlled by the Pagination's currentPageIndexProperty. There's a listener on that property that regenerates the FilteredList's predicate property each time the page is changed.

That handles the page changes, but what about sorting the whole list? First, the FilteredList is wrapped in a SortedList, and it's the SortedList that's set into the TableView. The SortedList's Comparator is bound to the TableView's Comparator.

But the SortedList only sees the Sensors included under the current filter. So a listener was added to the TableView's comparatorProperty. The action for this listener Streams the underlying ObservableList, sorts it using the new Comparator, and resets each Sensor's ordinalNumber according to the new sort order.

Finally, in order to have the FilteredList re-evaluate the ObservableList, these ordinalNumber changes need to trigger a ListChange event. So an extractor was added to the ObservableList based on the ordinalNumber.

The result works pretty well, except for the goofy page numbering Text sliding onto the screen with each page change.

The entire code was cleaned up for readability and unused stuff was stripped out to keep the example minimal.

Here's the Sensor class:

    class Sensor {
      public SimpleStringProperty userid;
      public SimpleStringProperty resourceid;
      public SimpleStringProperty sid;
      public SimpleStringProperty timestamp;
      public SimpleStringProperty reading;
      public IntegerProperty ordinalNumber = new SimpleIntegerProperty(0);
       
      public Sensor(int userid, String resourceid, String sid, String timestamp, String reading, String condition) {
          this.userid = new SimpleStringProperty(Integer.toString(userid));
          this.resourceid = new SimpleStringProperty(resourceid);
          this.sid = new SimpleStringProperty(sid);
          this.timestamp = new SimpleStringProperty(timestamp);
          this.reading = new SimpleStringProperty(reading);
          this.ordinalNumber.set(userid);
      }
    }
     

Here's the layout code:

public class PaginationController extends Application {
    public TableView<Sensor> sensorsTable = new TableView<>();
    public ObservableList<Sensor> sensorObservableList = FXCollections.observableArrayList(sensor -> new Observable[]{sensor.ordinalNumber});
    public FilteredList<Sensor> sensorFilteredList = new FilteredList<>(sensorObservableList);
    public SortedList<Sensor> sensorSortedList = new SortedList<>(sensorFilteredList);
    public IntegerProperty currentPage = new SimpleIntegerProperty(0);
    public int rowsPerPage = 14;

    public void start(final Stage stage) throws Exception {
        processSensors();
        stage.setScene(new Scene(buildScene(), 1024, 768));
        stage.setTitle("Table pager");
        stage.show();
    }

    public Region buildScene() {
        buildTable();
        int numOfPages = calculateNumOfPages();
        Pagination pagination = new Pagination((numOfPages), 0);
        pagination.setPageFactory(pageIndex -> {
            Text text = new Text("This is page " + (pageIndex + 1));
            return text;
        });
        pagination.setMaxPageIndicatorCount(numOfPages);
        currentPage.bind(pagination.currentPageIndexProperty());
        sensorFilteredList.predicateProperty().bind(Bindings.createObjectBinding(() -> createPageFilter(pagination.getCurrentPageIndex()), pagination.currentPageIndexProperty()));
        return new VBox(sensorsTable, pagination);
    }

    @NotNull
    private Predicate<Sensor> createPageFilter(int currentPage) {
        int lowerLimit = (currentPage) * rowsPerPage;
        int upperLimit = (currentPage + 1) * rowsPerPage;
        return sensor -> (sensor.ordinalNumber.get() >= lowerLimit) &&
                (sensor.ordinalNumber.get() < upperLimit);
    }

    private int calculateNumOfPages() {
        int numOfPages = 1;
        if (sensorObservableList.size() % rowsPerPage == 0) {
            numOfPages = sensorObservableList.size() / rowsPerPage;
        } else if (sensorObservableList.size() > rowsPerPage) {
            numOfPages = sensorObservableList.size() / rowsPerPage + 1;
        }
        return numOfPages;
    }

    public void processSensors() {
        Random random = new Random();
        for (int i = 0; i < 60; i++) {
            sensorObservableList.add(new Sensor(i, "rid-" + i, "sid-" + i, Integer.toString(random.nextInt(100)), "0", "no condition"));
        }
    }


    public void buildTable() {
        addStringColumn("userid", param1 -> param1.getValue().userid);
        addStringColumn("resourceid", param1 -> param1.getValue().resourceid);
        addStringColumn("sid", param1 -> param1.getValue().sid);
        addStringColumn("timestamp", param1 -> param1.getValue().timestamp);
        addStringColumn("reading", param1 -> param1.getValue().reading);
        TableColumn<Sensor, Number> ordinalCol = new TableColumn<>("ordinal");
        ordinalCol.setCellValueFactory(param -> param.getValue().ordinalNumber);
        ordinalCol.setPrefWidth(100);
        sensorsTable.getColumns().add(ordinalCol);
        sensorsTable.setItems(sensorSortedList);
        sensorSortedList.comparatorProperty().bind(sensorsTable.comparatorProperty());
        sensorSortedList.comparatorProperty().addListener(x -> renumberRecords());
        
    }

    private void renumberRecords() {
        AtomicInteger counter = new AtomicInteger(0);
        Comparator<Sensor> newValue = sensorsTable.getComparator();
        if (newValue != null) {
            sensorObservableList.stream().sorted(newValue).forEach(sensor -> sensor.ordinalNumber.set(counter.getAndIncrement()));
        } else {
            sensorObservableList.forEach(sensor -> sensor.ordinalNumber.set(counter.getAndIncrement()));
        }
    }

    @NotNull
    private void addStringColumn(String columnTitle, Callback<TableColumn.CellDataFeatures<Sensor, String>, ObservableValue<String>> callback) {
        TableColumn<Sensor, String> column = new TableColumn<>(columnTitle);
        column.setCellValueFactory(callback);
        column.setPrefWidth(100);
        sensorsTable.getColumns().add(column);
    }
}

For demonstration purposes, the timestamp field in the Sensor was initialized to a random number so that it would give an obvious change when that column was sorted. Also, the ordinalNumber field was added to the table so that it could be easily verified that they had been re-evaluated when a new sort column was chosen.

DaveB
  • 1,836
  • 1
  • 15
  • 13
  • good to use a filteredList - bad to change the data object for view reasons ;) To solve the problem in the view realm exclusively, you could set the filtered as table items, bind the sorted comparator as usual to the table's and implement a custom sort policy that searches up the transformationLists until it finds the sorted (c&p of default, just add a line for lookup the sources). Now a bit of logic (not inside the data object) to "remember" a shown item before sort and adjusts the page to keep it showing – kleopatra Jul 31 '21 at 14:47
  • @kleopatra Generally I agree, but as far as I'm concerned, putting "data" objects in a TableView is not something to be done. So, by definition, any structure put into a TableView is essentially a View Model, and can freely be restructured to meet the needs of the GUI. – DaveB Jul 31 '21 at 14:57
  • hmm .. always possible to fly to Rome via Tokyo ;) And doesn't really matter, IMO: even if the real data is wrapped into a viewmodel, adding properties dictated by a _single instance_ of a view is bad (making the item not reusable across different tables/paginations). – kleopatra Jul 31 '21 at 15:32
  • btw: I'm getting an NPE at the third click (change to unsorted) – kleopatra Jul 31 '21 at 15:42
  • Doh! I've fixed it. – DaveB Jul 31 '21 at 16:14
  • Thanks for the answer! I m not very familiar with the intricate parts of JavaFX so I couldnt really achieve what I wanted. I find that the sorting isnt working perfectly yet, but i m assuming thats because the comparator isnt able to sort strings properly. – experiment unit 1998X Jul 31 '21 at 16:15
  • 1
    The comparator absolutely sorts strings properly. If you want to sort on numeric values as String, you'll need to do some extra work. My recommendation would be to leave the numeric values as numeric values in your Table Model, but then format them as you want in the TableCells. Leave the data as it should be, and let the presentation layer deal with the presentation. – DaveB Jul 31 '21 at 16:19
  • @DaveB can i ask regarding `sensorFilteredList.predicateProperty().bind(Bindings.createObjectBinding(() -> createPageFilter(pagination.getCurrentPageIndex()), pagination.currentPageIndexProperty()));` what is the currentPageIndexProperty() used for here? I understand that the createPageFilter here will filter out the other items that are not on current page. – experiment unit 1998X Jul 31 '21 at 17:00
  • 1
    @experiment unit 1998X That's from the Bindings library. It takes 2 or more parameters, the first is essentially the `onChange()` code of a custom Binding, and the 2nd and later are the properties that trigger the execution of the code in the first parameter. So in this case, any change to the `pagination.currentPageIndexProperty()` will trigger the Binding to update the FilteredList's predicate property. – DaveB Jul 31 '21 at 17:04
  • @kleopatra Your suggestion wouldn't work. Specifically, the "remember" an item on the screen and keep it showing. Let's say we're on page 5, when the sort order is changed, there's no guarantee that anything that was on page 5 before would still be on page 5 afterwards, and all of the things on that page may move to different pages. So no possible logic to pick "something" and keep it on the screen that would make any sense. Maybe if a row was selected, you could add some logic to find what page it was on...but if not? And it's not clear that changing the page would be desirable either. – DaveB Jul 31 '21 at 17:09
  • I see! Since the FilteredList is wrapping the original observableList, the changes will then be reflected onto the tableview. However, the TableView isnt really "changing". The pagination is only changing the label text, so its the FilteredList that filters out only a subset of the items to show. without the FilteredList, the tableview would be essentially just one long long tableview. – experiment unit 1998X Jul 31 '21 at 17:11
  • @kleopatra As to adding properties to the Model to support a single instance of a view. I've always felt that the View dictates the structure of the Model, not that the Model is designed in a vaccuum. Having properties related to a specific View can make sense, and other Views can just ignore them. What if another View needed an extra property for another column? Having actual multiple instances of Views running simultaneously off the same Model is a huge complication in any event. – DaveB Jul 31 '21 at 17:17
  • @experimentunit1998X Exactly. In truth, since you're stuck using a ChangeListener on the sort order, you could probably get the approach you had originally taken to work by streaming through the main list, sorting it according to the TableView and then pulling out the ones that should be in the table. But then, instead of using using TableView.setItems (which might possibly work without resetting the sort), use TableView.getItems().setAll(). – DaveB Jul 31 '21 at 17:22
  • hmm .. slightly confused: the requirement is to sort the complete list (vs. only the current page), isn't it? If so, this might not be working reliably: run, keep on first page, click on first column: expected/actual - string sorting of the ids like 1x, 2x ... click again on first column: expected 9x, 8x, 7 ... actual 2x, 1x - seems to be the first page in invers order. What am I missing? – kleopatra Aug 02 '21 at 14:02
  • @kleopatra I was slightly confused too. I changed the listener on the comparator to an invalidation listener and it seems to work properly now. – DaveB Aug 03 '21 at 17:38