简体   繁体   中英

How to bind a List<String> to a ObservableList<String>?

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?

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() . Ie 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; ie 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, eg

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM