J-bob
J-bob

Reputation: 9106

TornadoFX/JavaFX - filter an observable list based on another observable property

I'm having some difficulty figuring out a relatively simple filtering configuration with TorandoFX. I would like to create a FilteredList (backed by an ObservableList) based on a SimpleStringProperty. The filter operation should be "bound" to the string property, so that any updates to the property automatically re-execute the filter operation.

For example, say I want to filter the list based on the length of the string property so that all elements in the FilteredList have length >= the string property. The following does not work.

val prop = SimpleStringProperty()
val baseList = listOf("a", "aa", "aaa", "b", "bb", "bbb")
val filteredList = FilteredList(baseList){ t -> prop.length().lessThanOrEqualTo(t.length).get()}

I hooked this interface into a GUI, but as I type into the textfield (bound to the SimpleStringProperty the combobox (bound to the filteredList) does not change.

How do I make this code work?

Upvotes: 0

Views: 930

Answers (2)

J-bob
J-bob

Reputation: 9106

I figured it out. Thanks to James_D for pointing me in the right direction with Predicates. And thanks to others who have provided examples in JavaFX (which guided me to the TornadoFX/Kotlin answer).

Here's the answer in Kotlin:

val prop = SimpleStringProperty()
val baseList = listOf("a", "aa", "aaa", "b", "bb", "bbb")
val filteredList = SortedFilteredList(baseList).apply {
        filterWhen(prop) {prop, item -> (prop?.length ?: 0) <= item.length}
}

The magic here is the filterWhen (see docs) method. For reasons I don't understand, it's only available on a SortedFilteredList, not a plain FilteredList. filterWhen lets you explicitly declare what properties are to be observed for change and the filter re-run each time.

Upvotes: 1

James_D
James_D

Reputation: 209330

I don't know Kotlin/TornadoFX, but here's a JavaFX solution you (or others) may be able to translate.

The basic idea is to create the FilteredList and bind its predicateProperty to a Predicate that depends on the appropriate StringProperty. There are various library methods for creating such a binding. E.g. you can do:

filteredList = new FilteredList<>(baseList);
filteredList.predicateProperty().bind(
    new ObjectBinding<>() {
        {
            super.bind(prop);
        }
        @Override
        public Predicate<String> computeValue() {
            return t -> t.length() > prop.get().length() ;
        }
    }
);

You can also use the Bindings.createBinding() method, which takes a Callable<Predicate<String>> and a list of observables to observe (and if any are invalidated, recompute):

filteredList.predicateProperty().bind(Bindings.createObjectBinding(
    // Callable<Predicate<String>> expressed as a lambda: () -> Predicate<String>
    () ->
        // Predicate<String> expressed as a lambda: String -> boolean
        t -> t.length() > prop.get().length(),
    prop
));

Without the commentary, that reduces to the concise (but mind-boggling)

filteredList.predicateProperty().bind(Bindings.createObjectBinding(
    () -> t -> t.length() > prop.get().length(),
    prop
));

Here's a complete example:

import static javafx.beans.binding.Bindings.createObjectBinding;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class FilteredListExample extends Application {

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

        ObservableList<String> baseList = FXCollections.observableArrayList("a", "aa", "aaa", "b", "bb", "bbb");
        FilteredList<String> filteredList = new FilteredList<>(baseList);

        ListView<String> listView = new ListView<>(filteredList);

        TextField input = new TextField();

        filteredList.predicateProperty().bind(createObjectBinding(
                () -> t -> t.length() >= input.getText().length(),
                input.textProperty()));


        BorderPane root = new BorderPane(listView, input, null, null, null) ;
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

}

Upvotes: 1

Related Questions