I have a case where I need to filter a ObservableList<Item>
based on some properties of the items (i.e. the condition is internal and not external). I found out that javafx has FilteredList
so I tried it. I could set the predicate and filtering works, until the property value that determines the filtering changes. Setting the predicate is done now like following:
list.setPredicate(t -> !t.filteredProperty().get())
Since the predicate returns boolean and not BooleanProperty, the changes to that property are not reflected on the list.
Is there any easy solution to this? I could try to do some workarounds, e.g. create a separate list and sync that, or reset the predicate every time the property changes in one item hopefully retriggering the filtering, but I first wanted to ask in case someone knows a pretty solution as those workarounds certainly are not.
Create the underlying list with an extractor. This will enable the underlying list to fire update events when the filteredProperty()
of any elements change. The FilteredList
will observe these events and so will update accordingly:
ObservableList<Item> baseList = FXCollections.observableArrayList(item ->
new Observable[] {item.filteredProperty()});
FilteredList<Item> list = new FilteredList<>(baseList, t -> ! t.filteredProperty().get());
Quick demo:
import java.util.stream.IntStream;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
public class DynamicFilteredListTest {
public static void main(String[] args) {
ObservableList<Item> baseList = FXCollections.observableArrayList(item ->
new Observable[] {item.filteredProperty()});
FilteredList<Item> list = new FilteredList<>(baseList, t -> ! t.isFiltered());
list.addListener((Change<? extends Item> c) -> {
while (c.next()) {
if (c.wasAdded()) {
System.out.println(c.getAddedSubList()+ " added to filtered list");
}
if (c.wasRemoved()) {
System.out.println(c.getRemoved()+ " removed from filtered list");
}
}
});
System.out.println("Adding ten items to base list:\n");
IntStream.rangeClosed(1, 10).mapToObj(i -> new Item("Item "+i)).forEach(baseList::add);
System.out.println("\nFiltered list now:\n"+list);
System.out.println("\nSetting filtered flag for alternate items in base list:\n");
IntStream.range(0, 5).map(i -> 2*i).mapToObj(baseList::get).forEach(i -> i.setFiltered(true));
System.out.println("\nFiltered list now:\n"+list);
}
public static class Item {
private final StringProperty name = new SimpleStringProperty() ;
private final BooleanProperty filtered = new SimpleBooleanProperty() ;
public Item(String name) {
setName(name);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final BooleanProperty filteredProperty() {
return this.filtered;
}
public final boolean isFiltered() {
return this.filteredProperty().get();
}
public final void setFiltered(final boolean filtered) {
this.filteredProperty().set(filtered);
}
@Override
public String toString() {
return getName();
}
}
}
If you are using database loading function plus need to filter multiple fields, this solution will help.
ObservableList<PurchaseOrder> poData = FXCollections.observableArrayList();
FilteredList<PurchaseOrder> filteredData;
private void load() {
PurchaseOrder po = new PurchaseOrder();
try {
poData = po.loadTable("purchase_orders", beanFields); // Database loading data
} catch (SQLException ex) {
Logger.getLogger(PurchaseOrdersController.class.getName()).log(Level.SEVERE, null, ex);
}
filteredData = new FilteredList<>(poData, t -> true); //Encapsulate data with filter
poTable.setItems(filteredData); //Load filtered data into table
//Set event trigger for all filter textboxes
txtFilter.textProperty().addListener(obs->{
filter(filteredData);
});
txtFilter2.textProperty().addListener(obs->{
filter(filteredData);
});
}
private void filter(FilteredList<PurchaseOrder> filteredData)
{
filteredData.setPredicate(PurchaseOrder -> {
// If all filters are empty then display all Purchase Orders
if ((txtFilter.getText() == null || txtFilter.getText().isEmpty())
&& (txtFilter2.getText() == null || txtFilter2.getText().isEmpty())) {
return true;
}
// Convert filters to lower case
String lowerCaseFilter = txtFilter.getText().toLowerCase();
String lowerCaseFilter2 = txtFilter2.getText().toLowerCase();
//If fails any given criteria, fail completely
if(txtFilter.getText().length()>0)
if (PurchaseOrder.get("vendor_name").toLowerCase().contains(lowerCaseFilter) == false)
return false;
if(txtFilter2.getText().length()>0)
if (PurchaseOrder.get("PONumber").toLowerCase().contains(lowerCaseFilter2) == false)
return false;
return true; // Matches given criteria
});
}