0

A list of basic values is filtered by a (changing) predicate. The FilteredList is mapped to TreeItems and this resulting list is then used as the root TreeItems children.

When a selection was made on the TreeTableView and afterwards the predicate changes, accessing the selected items results in a NullPointerException.

It seems to me that items contained in the change are null. Is there a design flaw in this coarse concept?

This does not happen for the classes TreeView and ListView.

I tried to produce a MCVE using https://github.com/TomasMikula/EasyBind for the mapping:

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.fxmisc.easybind.EasyBind;

import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Spinner;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App extends Application {

    // fields protect bound lists from GC
    private ObservableList<DataItem> itemizedDataPool;
    private FilteredList<Data> filteredDataPool;
    private ObservableList<Data> selectedData;

    static class Data {
        final int value;

        public Data(int value) {
            this.value = value;
        }
    }

    static class DataItem extends TreeItem<Data> {
        final Data data;

        public DataItem(Data data) {
            this.data = data;
        }
    }

    @Override
    public void start(Stage primaryStage) throws IOException {

        List<Data> dataPool = new ArrayList<Data>();
        for (int i = 1; i < 20; i++) {
            dataPool.add(new Data(i));
        }

        filteredDataPool = new FilteredList<>(FXCollections.observableArrayList(dataPool));

        TreeTableView<Data> listView = createTreeTableView();
        Spinner<?> lowerBoundSelector = createLowerBoundFilter();
        Label sumLabel = createSummarizingLabel(listView.getSelectionModel().getSelectedItems());

        Parent root = new VBox(listView, lowerBoundSelector, sumLabel);

        Scene scene = new Scene(root, 768, 480);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private TreeTableView<Data> createTreeTableView() {
        itemizedDataPool = EasyBind.map(filteredDataPool, DataItem::new);
        TreeItem<Data> itemRoot = new TreeItem<>();
        Bindings.bindContent(itemRoot.getChildren(), itemizedDataPool);

        TreeTableView<Data> listView = new TreeTableView<>(itemRoot);
        listView.setShowRoot(false);
        itemRoot.setExpanded(true);
        listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        listView.getColumns().add(new TreeTableColumn<>("Data"));
        return listView;
    }

    private Label createSummarizingLabel(ObservableList<TreeItem<Data>> selectedItems) {
        Label sumLabel = new Label();
        selectedData = EasyBind.map(selectedItems, (TreeItem<Data> t) -> ((DataItem) t).data);
        selectedData.addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                int sum = 0;
                for (Data d : selectedData) {
                    sum += d.value;
                }
                sumLabel.setText("Sum: " + sum);
            }
        });
        return sumLabel;
    }

    private Spinner<Integer> createLowerBoundFilter() {
        Spinner<Integer> lowerBoundSelector = new Spinner<>(0, 20, 0, 1);
        lowerBoundSelector.valueProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                filteredDataPool.setPredicate(t -> t.value > lowerBoundSelector.getValue());
            }
        });
        return lowerBoundSelector;
    }

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

}
Miss Chanandler Bong
  • 4,081
  • 10
  • 26
  • 36
JD3
  • 33
  • 1
  • 7
  • Well last time I checked the `ListIterator.nextIndex` method of a ListIterator somewhere in of those lists returned by the selection model was broken (don't know if they have checked my bug report and fixed that error). This could be the source of the error. – fabian Mar 03 '16 at 19:07
  • @fabian Do you have a link to this bug report? – JD3 Mar 03 '16 at 22:08
  • Bug report: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8145887 Unfortunately they won't let me comment on this (or at least I haven't found out how to...), since I've identified the source: compare the indices used by `next()` and the index returned by `nextIndex` here: http://grepcode.com/file/repo1.maven.org/maven2/net.java.openjfx.backport/openjfx-78-backport/1.8.0-ea-b96.1/com/sun/javafx/scene/control/ReadOnlyUnbackedObservableList.java#ReadOnlyUnbackedObservableList.SelectionListIterator.nextIndex%28%29 – fabian Mar 03 '16 at 22:29
  • Yes, this is indeed wrong. `nextIndex` should return `pos` directly, as it always points to the "next" index. But I don't see a trivial connection between this bug and the bug I'm dealing with. As noted in the text, my problem only occurs for `TreeTableView`s and not for `TreeView`s or `ListView`s. The above code does work perfectly for the latter two classes. – JD3 Mar 04 '16 at 07:41
  • `TableViewSelectionModel` extends `MultipleSelectionModel` which uses `ReadOnlyUnbackedObservableList`. – fabian Mar 04 '16 at 07:58
  • I see. Thank you for your explanation. – JD3 Mar 04 '16 at 11:24

1 Answers1

0

Problem

TreeTableView uses TreeTableViewArrayListSelectionModel, which extends MultipleSelectionModelBase, which uses ReadOnlyUnbackedObservableList, which uses (and contains) SelectionListIterator, which has a broken implementation for its method nextIndex.

Thanks to fabian for pointing that out. He also filed a bug report (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8145887).

Workaround

Using a buffer in between could provide an effective workaround for the problem above. I tried several approaches. setAll on selection invalidation and Bindings.bindContent do not work. In both cases I received null values in the list. The straightforward "solution" is to simply filter the nulls out. This leads to the inefficient but apparently effective code below.

// [...]
TreeTableView<Data> listView = createTreeTableView();
selectionBuffer = FXCollections.observableArrayList();
listView.getSelectionModel().getSelectedItems().addListener(new InvalidationListener() {
    @Override
    public void invalidated(Observable observable) {
        selectionBuffer.clear();
        for (TreeItem<Data> t : listView.getSelectionModel().getSelectedItems()) {
            if (t != null) {
                selectionBuffer.add(t);
            }
        }
    }
});
// [...]

Using selectionBuffer instead of listView.getSelectionModel().getSelectedItems() should now compensate the implementation problem in nextIndex.

Community
  • 1
  • 1
JD3
  • 33
  • 1
  • 7