0

I have an ObservableSet<DoubleProperty> itemSet which holds the DoubleProperty prop1 for any number of instances of Item.

I want to create another DoubleProperty total that will reflect an up to date total of all DoubleProperty's in itemSet.

The double value of each DoubleProperty in the set can change independently. The total value would need to reflect those changes.

This is the Item class:

class Item {
    DoubleProperty prop1;
    DoubleProperty prop2;

    public Item() {
        this.prop1 = new SimpleDoubleProperty(1.0);
        this.prop2 = new SimpleDoubleProperty(2.0);

        itemSet.add(this.prop1);
    }
}

This is a sort of global variable class...

class ItemValue {
    private ItemValue itemValue = null;

    ObservableSet<DoubleProperty> itemSet = FXCollections.observableSet();
    DoubleProperty total;

    private ItemValue() {
        this.total = new SimpleDoubleProperty(0.0);

        // create several Item's here...

        itemSet.addListener((InvalidationListener) observable -> {
        /*
        Something which binds the total
        I figure it will need to go here so that if new items get added the total will reflect that?
        */
        });
    }

    public ItemValue get() {
        if (itemValue == null) itemValue = new ItemValue();
        return itemValue;
}
Ian Competent
  • 93
  • 1
  • 1
  • 6

2 Answers2

2

As far as I know there is no built in way to do this simply. However, there's a couple of ways you could do this. The most(?) efficient, but more complicated way would be to listen to the ObservableSet for additions/removals, observe any current DoubleProperty elements, and modify the total property yourself.

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;

public class SomeClass {

  private final ReadOnlyDoubleWrapper total = new ReadOnlyDoubleWrapper(this, "total");
  private void setTotal(double total) { this.total.set(total); }
  public final double getTotal() { return total.get(); }
  public final ReadOnlyDoubleProperty totalProperty() { return total.getReadOnlyProperty(); }

  private final ObservableSet<DoubleProperty> propertySet = FXCollections.observableSet();
  private final ChangeListener<Number> elementListener = this::elementValueChanged;
  private final WeakChangeListener<Number> weakElementListener =
      new WeakChangeListener<>(elementListener);

  public SomeClass() {
    propertySet.addListener(this::propertySetChanged);
  }

  private void propertySetChanged(SetChangeListener.Change<? extends DoubleProperty> change) {
    if (change.wasRemoved()) {
      change.getElementRemoved().removeListener(weakElementListener);
      setTotal(getTotal() - change.getElementRemoved().get());
    }
    if (change.wasAdded()) {
      change.getElementAdded().addListener(weakElementListener);
      setTotal(getTotal() + change.getElementAdded().get());
    }
  }

  private void elementValueChanged(ObservableValue<? extends Number> observable,
                                   Number oldValue, Number newValue) {
    setTotal(getTotal() - oldValue.doubleValue() + newValue.doubleValue());
  }

}

Here the SetChangeListener, whose value is a method reference to propertySetChanged, watches for any changes to the ObservableSet. When a DoubleProperty is added it adds said property's value to the current total. When a DoubleProperty is removed it subtracts said property's value from the current total. This listener also adds or removes a ChangeListener to or from the DoubleProperty when it is added or removed from the ObservableSet, respectively.

The ChangeListener, whose value is a method reference to elementValueChanged, updates the total property when the value of any DoubleProperty changes. It does this by first subtracting the old value and then adding the new value to the current total. It is actually the WeakChangeListener, which wraps the original ChangeListener, that is added or removed. This helps avoid potential memory leaks. Remember to maintain a strong reference to the original ChangeListener when using WeakChangeListener otherwise the original ChangeListener may be garbage collected too soon.

A second option is to rebuild a binding every time the ObservableSet is invalidated and then bind the total property to said binding.

import javafx.beans.Observable;
import javafx.beans.binding.DoubleExpression;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;

public class SomeClass {

  private final ReadOnlyDoubleWrapper total = new ReadOnlyDoubleWrapper(this, "total");
  private void setTotal(double total) { this.total.set(total); }
  public final double getTotal() { return total.get(); }
  public final ReadOnlyDoubleProperty totalProperty() { return total.getReadOnlyProperty(); }

  private final ObservableSet<DoubleProperty> propertySet = FXCollections.observableSet();

  public SomeClass() {
    propertySet.addListener(this::propertySetInvalidated);
  }

  private void propertySetInvalidated(Observable observable) {
    if (propertySet.isEmpty()) {
      total.unbind();
      setTotal(0.0);
    } else if (propertySet.size() == 1) {
      total.bind(propertySet.iterator().next());
    } else {
      DoubleExpression sum = null;
      for (DoubleProperty property : propertySet) {
        sum = (sum != null) ? sum.add(property) : property;
      }
      total.bind(sum);
    }
  }

}

In this case we add an InvalidationListener to the ObservableSet. This listener will be invoked whenever an element(s) is added to or removed from the ObservableSet. When this happens 1 of 3 things will happen:

  1. If the ObservableSet is now empty unbind the total property and set it to zero.
    • This is a special case to deal with no elements
  2. If the ObservableSet now only contains a single element simply bind the total property to said element.
    • Another special case dealing with a single element. It stops us from creating unnecessary objects and preforming unnecessary computations that would happen if we just skipped to the third branch.
  3. Otherwise create one big binding that calculates the sum and then bind total to that binding.
    • This branch uses DoubleExpression.add(ObservableNumberValue). The resulting DoubleBinding from that method call will update whenever one of the two observables change. This is reduced into a single DoubleExpression which we then bind the total property to.

This second option will be less efficient because it requires iterating the entire ObservableSet every time. It also potentially leads to a lot of DoubleBinding objects being created. However, you may find it simpler to code/understand and the performance hit may not be significant enough for your application.

Slaw
  • 37,820
  • 8
  • 53
  • 80
0

I would give the itemSet a change listener and just recalculate the total with some method every time its called.

See https://stackoverflow.com/a/44141262/8729420.

faris
  • 692
  • 4
  • 18
  • Hmmm...not sure that will work. If I understand correctly, a `ChangeListener` on the set will only recognise if a `DoubleProperty` in the set is added/removed/replaced, but not if the value of that `DoubleProperty` is changed. Hence my problem: I specifically need to total the value of those `DoubleProperty`'s – Ian Competent Sep 05 '18 at 00:28