0

I have an object Bean containing a List<String>. I would like to "bind" this list to an ObservableList so when an item is added to or removed from the original list, the ObservableList is updated (which then triggers the listeners that monitor the ObservableList).

I found this question whose answer shows how to wrap a simple String into a JavaFX StringProperty using JavaBeanStringPropertyBuilder.

I tried to do the same thing but replacing the String with a List<String> as shown below:

public class Bean {

    private final List<String> nameList;
    private final PropertyChangeSupport propertySupport ;

    public Bean() {
        this.nameList = new ArrayList<>();
        this.propertySupport = new PropertyChangeSupport(this);
    }

    public List<String> getNameList() {
        return nameList;
    }

    public void setNameList(List<String> nameList)
    {
        List<String> oldList = new ArrayList<>(this.nameList);
        this.nameList.clear();
        this.nameList.addAll(nameList);
        propertySupport.firePropertyChange("nameList", oldList, this.nameList);
    }

    public void addName(String name) {
        List<String> oldList = new ArrayList<>(this.nameList);
        this.nameList.add(name);
        propertySupport.firePropertyChange("nameList", oldList, this.nameList);
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propertySupport.addPropertyChangeListener(listener);
    }
}
Bean bean = new Bean();
JavaBeanObjectProperty<List<String>> listProperty = null;
try
{
    listProperty = JavaBeanObjectPropertyBuilder.create().bean(bean).name("nameList").build();
} catch (NoSuchMethodException e)
{
    throw new RuntimeException(e);
}
listProperty.addListener((ObservableValue<? extends List<String>> obs, List<String> oldName, List<String> newName) ->
{
    System.out.println("List changed");
});

bean.setNameList(Arrays.asList("George", "James"));

But the listener is not triggered after calling setNameList() and I don't know what I'm missing. Could you help me please?

Scytes
  • 13
  • 5
  • 1
    Is this just an example or are you trying to use a Class to do list operations? If this is not just an example avoid using a class to do list operations and in this case, just use an `ObservableList`. – SedJ601 Dec 08 '22 at 17:03
  • 1
    I would also suggest you try to follow this pattern. -> https://stackoverflow.com/questions/32342864/applying-mvc-with-javafx – SedJ601 Dec 08 '22 at 17:05
  • @SedJ601 This is just an example. In my real application, the `List` is an attribute of a class in the model and the `ObservableList` is an attribute of a class in the view. And I'm not allowed to use JavaFx classes in my model. – Scytes Dec 08 '22 at 17:22
  • 1
    "I'm not allowed to use JavaFX classes in my model". That doesn't make any sense, to be honest. You should not use UI classes in your model, but `ObservableList` is not a UI class, it represents data. The "observable" part of the list is *specifically* designed for use in a model in a MVC architecture. If this is a homework-type restriction, I would seek clarification that you've understood it correctly. – James_D Dec 08 '22 at 17:28
  • 2
    Anyway: the listener will only be fired if there is an actual change. The underlying list reference is not changing: you are calling `propertySupport.firePropertyChange("nameList", oldList, nameList);` with `oldList == nameList`. I don't remember if the Java Beans property change will be fired, but even if it is it will cause `listProperty.set(nameList)` to be called, and since `nameList == listProperty.get()`, no change event will be fired be `listProperty`. – James_D Dec 08 '22 at 17:31
  • @James_D Indeed, the methods `setNameList()` and `addName()` were wrong. I fixed it in the original post. Concerning the "I'm not allowed to use JavaFX classes in my model" part, let me be more clear. The model part of my application must be able to be compiled with a standard JDK 11. JavaFx has been separated from the JDK, hence I am not allowed to reference JavaFX classes in my model package. I hope this justification makes more sense to you. – Scytes Dec 08 '22 at 17:46
  • 1
    The edit doesn't change anything. In the `listProperty`, the new list that is being set is still a reference to the "current" list value. The call to `firePropertyChange(...)` arguably breaks the contract, because `oldList` is not a reference to the previous value (it's a reference to a new list, which you discard), whereas `nameList` is a reference to the previous value. – James_D Dec 08 '22 at 17:47
  • @James_D I forgot to precise: even with the correction on both methods, the listener is still not triggered. I think the problem may come from the way I use `JavaBeanObjectPropertyBuilder`. – Scytes Dec 08 '22 at 17:48
  • So if you switch things around to the "natural" way of doing things, i.e. your `setNameList(List nameList)` would do `List oldList = this.nameList;`, then `this.nameList = nameList ;`, and finally `firePropertyChange("nameList", oldList, nameList);` I believe it will work. `addName(...)` will be pretty awful, as you'll have to create an entire new list. – James_D Dec 08 '22 at 17:50
  • 2
    The justification still doesn't make sense. The MVC architecture is an implementation of the presentation tier, so the "model" in the MVC sense is a part of the presentation tier. If you're using JavaFX for your presentation tier, then it makes no sense to me to require that parts of the presentation tier have to be compiled without JavaFX. I can see having that requirement for the business tier or data access tier, etc., but the (MVC) model is not part of those tiers. Isn't `PropertyChangeSupport` part of AWT (or Swing), which is part of `java.desktop`? It makes no less sense to require that. – James_D Dec 08 '22 at 17:53

1 Answers1

1

A change listener registered with a Property<T> will only be notified if the value of the property actually changes. That is, the set(T newValue) method is implemented something like this:

public void set(T newValue) {
    T oldValue = this.get();
    if (! oldValue.equals(newValue)) {
        // notify change listeners...
    }
}

The JavaBeanObjectPropertyBuilder is going to create a JavaBeanObjectProperty<List<String>> (an implementation of Property<List<String>>) and set its value to the result of calling bean.getNameList(). I.e. the value held internally by listProperty is a reference to bean.nameList.

The JavaBeanObjectProperty also registers a listener via the call to bean.addPropertyChangeListener(...). When

propertySupport.firePropertyChange("nameList", oldList, this.nameList);

is invoked, the internal listener in JavaBeanObjectProperty will set its own value to the new value fired by the property change support; i.e. it will call

set(bean.nameList);

However, since this is just a reference to the current value of the property, no change listener registered with listProperty will be notified (basically, no change has occurred).

To clarify, if it helps: the content of the List<String> returned by listProperty.get() will change when you call

this.nameList.clear();

and

this.nameList.addAll(nameList);

in the bean (because the listProperty references bean.nameList), but the actual list reference itself has not changed.

You can test this with, e.g.

Bean bean = new Bean();
JavaBeanObjectProperty<List<String>> listProperty = null;
try
{
    listProperty = JavaBeanObjectPropertyBuilder.create().bean(bean).name("nameList").build();
} catch (NoSuchMethodException e)
{
    throw new RuntimeException(e);
}
listProperty.addListener((ObservableValue<? extends List<String>> obs, List<String> oldName, List<String> newName) ->
{
    System.out.println("List changed");
});

List<String> oldList = listProperty.get();

bean.setNameList(Arrays.asList("George", "James"));

List<String> newList = listProperty.get();

System.out.println(oldList);
System.out.println(newList);
System.out.println(oldList == newList);

The best fix is simply to use an ObservableList in your Bean class:

public class Bean {

    private final ObservableList<String> nameList;

    public Bean() {
        this.nameList = FXCollections.observableArrayList<>();
    }

    public ObservableList<String> getNameList() {
        return nameList;
    }


    public void addName(String name) {
        this.nameList.add(name);
    }

}

Note you don't lose the functionality provided by setNameList(...); you can do

bean.getNameList().setAll(...);

if you want to set the entire content of the list. If you want the same API, you can use a ListProperty instead of the ObservableList.

The test code you have then becomes

Bean bean = new Bean();

bean.getNameList().addListener((ListChangeListener.Change<? extends String> change) ->
{
    System.out.println("List changed");
});

bean.getNameList().setAll("George", "James");

As stated in the comments in the question, I don't really understand having any restriction preventing the use of ObservableList in the model; indeed this is exactly the use case for which ObservableList (along with the properties and bindings API) was designed.

There is no adapter designed for use with observable lists in the same way as there are Java Bean adapters for simple properties. Thus if you really wanted to avoid use of ObservableList in your model class (which, again, doesn't really make sense to me), you would have to implement your own listener notification for the Bean:

public class Bean {

    private final List<String> nameList ;
    private final List<Consumer<String>> nameAddedListeners ;
    private final List<Consumer<List<String>>> nameListReplacedListeners ;

    public Bean() {
        this.nameList = new ArrayList<>();
        this.nameAddedListeners = new ArrayList<>();
        this.nameListReplacedListeners = new ArrayList<>();
    }

    public List<String> getNameList() {
        return nameList ;
    }

    public void setNameList(List<String> newNames) {
        this.nameList.setAll(newNames);
        nameListReplacedListeners.forEach(listener -> listener.accept(newNames));
    }

    public void addName(String name) {
        this.nameList.add(name);
        nameAddedListeners.forEach(listener -> listener.accept(name));
    }

    public void addNameListReplacedListener(Consumer<List<String>> listener) {
        nameListReplacedListeners.add(listener);
    }

    public void addNameAddedListener(Consumer<String> listener) {
        nameAddedListeners.add(listener);
    }
}

Now you could do

Bean bean = new Bean();
bean.addNameListReplacedListener(list -> System.out.println("Names changed"));
bean.setNameList(List.of("George", "James"));

or you could effectively create an adapter:

Bean bean = new Bean();
ObservableList<String> names = FXCollections.observableArrayList(bean.getNameList());
bean.addNameAddedListener(names::add);
bean.addNameListReplacedListener(names::setAll);

etc.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks a lot for your answer! I understand better now what exactly `JavaBeanObjectPropertyBuilder` does. I will come back to you when I have chosen the final implementation for my problem. Regarding the "no JavaFX in the model", I understand now that I used the wrong word. My `List` is not in the model layer of an MVC pattern but in the **business** layer of my application. Sorry, in my native language we use the same word for both model and business layer so I used it wrong here. – Scytes Dec 09 '22 at 09:41
  • @Scytes In that case, I would probably create a `Bean` model class in the presentation tier, using `ObservableList` and JavaFX properties and bindings, etc. In the presentation tier, retrieve the "business beans" from the business tier, and convert them to "presentation beans" for use in the presentation tier. It adds a layer, but it will make the JavaFX code much simpler. – James_D Dec 09 '22 at 14:47