JavaFX: BooleanBindings in bind with multiple EasyBind maps

627 Views Asked by At

This question goes further where JavaFX: Disable multiple rows in TableView based on other TableView stops. I want to generate a more general topic, of which other people could also benefit.

I also have the two tableviews. I also want to disable a row in table2 when table1 contains the same object. This is achieved with the code below:

//Check if a row needs to be disabled: this is achieved with a rowfactory
ObservableList<String> listOfSKUs = EasyBind.map(table1.getItems(),Product::getSKU);
table2.setRowFactory(tv -> {

    TableRow<Product> row = new TableRow<>();

    //The row doesnt need to be checked when it is empty, thats the first part
    //The second part is a big OR: the row is a gift card OR the listOfSkus contains the row. In this last one, the amount also needs to be checked.

    row.disableProperty().bind(Bindings.createBooleanBinding( () -> 
        row.getItem() != null && 
        (row.getItem().getProductName().equals("Kadobon") ||
        (listOfSKUs.contains(row.getItem().getSKU()) //&& some part to check for the amount)  
        ), listOfSKUs, row.itemProperty() ));

     return row;

});

Now I only want the row to be disabled when:

  1. The SKU of the product from table1 is also in table2 (this works now)
  2. The amount of the product in table2 is negative

I do not know how to check on the amount, because I need the amount that is associated with the certain SKU. How can I create this with multiple maps in one BooleanBinding?

Any help is greatly appreciated!

1

There are 1 best solutions below

6
On BEST ANSWER

This sounds to me that you might benefit from reconsidering your object model. It seems that your Product has two different values you refer to as amount: one that's displayed in table1, and one that's displayed in table2. If you make those two different fields of the Product class, then all this becomes much easier, as you can represent the items in both tables with the same actual objects (just with different properties displayed in the columns); and then the criteria for disabling a row becomes just a function of the object you're looking at (along with table2.getItems().contains(...)).

Another option might be an SKU class:

public class SKU {

    private final StringProperty sku = ... ;

    // Obviously use more meaningful names for these:
    private final ObjectProperty<Product> table1Product = ... ;
    private final ObjectProperty<Product> table2Product = ... ;

    // getters, setters etc...

}

Then both tables can be TableView<SKU>s with the cell value factories on the columns just looking at the properties from the correct product. That way row.getItem() in table1's row factory returns the SKU object, and can look at the table2 properties as needed.

If you really can't do that, then one way to do this would be to maintain an ObservableMap<String, Double> which maps SKUs to the amount displayed in table2. This is a bit of work, but not too bad:

ObservableMap<String, Double> skuAmountLookup = table2.getItems().stream().collect(
    Collectors.toMap(Product::getSKU, Product::getAmount,
    // the next argument is a merge function, which is used if there are two (or more) items
    // in table2 with the same SKU. This function would replace the first value by the second.
    // If the SKUs are unique it doesn't really matter what you have here...
    (x, y) -> y,
    () -> FXCollections.observableHashMap()));

Now you need to keep the map updated when the table contents change:

table2.getItems().addListener((ListChangeListener.Change<? extends Product> change) -> {
    // can just do skuAmountLookup.clear(); followed by skuAmountLookup.putAll(...)
    // passing in the same data used above to initialize it
    // That's pretty inefficient though, the following will just update what's needed.
    // This assumes SKUs are unique in table 2:
    while (change.next()) {
        if (change.wasAdded()) {
            change.getAddedSubList().forEach(product -> 
                skuAmountLookup.put(product.getSKU(), product.getAmount()));
        }
        if (change.wasRemoved()) {
            change.getRemoved().forEach(product -> skuAmountLookup.remove(product.getSKU()));
        }
    }
});

And then the disable criteria can be something like

row.disableProperty().bind(Bindings.createBooleanBinding(
    () -> row.getItem() != null &&
          skuAmountLookup.containsKey(row.getItem().getSKU()) &&
          skuAmountLookup.get(row.getItem().getSKU()) < 0, 
    skuAmountLookup,
    row.itemProperty()));

I haven't tried any of this, but it should work. There are two assumptions here:

  1. SKUs are unique in table 2
  2. The amount in table 2 isn't going to change, other than by items being added or removed from the table. This can be worked around if needed, but it adds one more layer of complexity to the listener on table2's items.