'JavaFX: ObsevableMap keySet as an ObservableSet
I want to transform an ObservableMap's keySet to a read only ObservableSet. I don't want to copy the value, any modification to the ObservableMap must affect the Observable keySet. If i bind another set to the observable key set content, its content is automatically updated.
This is what i would like to write.
ObservableMap<String, Object> map = FXCollections.observableHashMap();
ObservableSet<String> keySet = FXCollections.observableKeySet(map);
Set<String> boundSet = new HashSet<String>();
Bindings.bindContent(boundSet, keySet);
map.put("v", new Object());
assert boundSet.contains("v");
Is there this functionality in the JavaFX SDK ?
Solution 1:[1]
The feature you request does not need a special ObservableSet
. It’s already part of the Map
interface contract:
ObservableMap<String, Object> map = FXCollections.observableHashMap();
Set<String> keySet = map.keySet();
map.put("v", new Object());
assert keySet.contains("v");
A Map
’s keyset always reflects the changes made to the backing map.
http://docs.oracle.com/javase/8/docs/api/java/util/Map.html#keySet--
Returns a
Set
view of the keys contained in this map. The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.
Solution 2:[2]
As far as I know, there's no built-in way to do it.
Here's a utility method that I made to handle this:
/**
* Builds and returns an observable keyset of the specified map. Note that the resulting
* keyset is a new set that is guaranteed to have the same items as the map's true
* keyset, but does not make any guarantees about iteration order or implementation
* details of the keyset (for example, if the Map is a SortedMap, the keyset
* will not necessarily maintain keys in sorted order).
* @param <K> Map's key type
* @param <V> Map's value type
* @param map the ObservableMap to which the set should be bound
* @return a new observable set reflecting the map's keys
*/
public static <K, V> ObservableSet<K> getObservableKeySet(ObservableMap<K, V> map) {
ObservableSet<K> set = FXCollections.observableSet(new HashSet<>());
map.addListener(
(MapChangeListener<K, V>)(
change -> {
if(change.wasAdded() && !change.wasRemoved()) {
set.add(change.getKey());
}
if(change.wasRemoved() && !change.wasAdded()) {
set.remove(change.getKey());
}
//Note that if change was added and removed, that means that
//the key was unchanged and a value was just replaced. That
//shouldn't affect the keyset so we do nothing
})
);
return set;
}
Update:
I decided that I didn't like how the iteration order of the resulting set wouldn't match the original map's iteration order, so instead I made a new class that acts as an observable wrapper around a map's keyset. This is more like Map's built-in keySet() method where it returns a view of the actual set. This just adds in the listeners that make it observable:
/**
* Observable view of an ObservableMap's keyset
*/
public class ObservableKeySet<K, V> implements ObservableSet<K> {
/**
* The map's actual keyset object that gets wrapped
*/
private Set<K> wrappedSet;
/**
* Invalidation listeners to be notified when the set changes. Note that we end
* up calling these more than we should since invalidation listeners should only
* be called if the observable value is observed between changes and we're going
* to call them on every change. However, the ObservableSet returned from
* FxCollectionUtilities.observableSet actually also does that too, so I feel
* like we can get away with it.
*/
private Collection<InvalidationListener> invalidationListeners = new ArrayList<>();
/**
* Change listeners to be notified when the set changes
*/
private Collection<SetChangeListener<? super K>> changeListeners = new ArrayList<>();
/**
* Creates an Observable Set view of the specified map's keyset
* @param map ObservableMap
*/
public ObservableKeySet(ObservableMap<K, V> map) {
this.wrappedSet = map.keySet();
map.addListener((MapChangeListener<K,V>)this::onMapChange);
}
/**
* Code to be executed on any match change. It will determine if there is a resulting
* set change and trigger listeners as appropriate
* @param change
*/
private void onMapChange(MapChangeListener.Change<? extends K, ? extends V> change) {
SetChangeListener.Change<K> setChange = null;
//Note that if the map change says that there was an add and removal, then
//that means a value was getting replaced, which doesn't result in a keySet
//change
if(change.wasAdded() && ! change.wasRemoved()) {
setChange = new BasicSetChange(true, change.getKey());
}
else if(change.wasRemoved() && ! change.wasAdded()) {
setChange = new BasicSetChange(false, change.getKey());
}
if(setChange != null) {
invalidationListeners.forEach(listener -> listener.invalidated(this));
final SetChangeListener.Change<K> finalChange = setChange;
changeListeners.forEach(listener -> listener.onChanged(finalChange));
}
}
@Override
public void addListener(InvalidationListener listener) {
invalidationListeners.add(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
invalidationListeners.remove(listener);
}
@Override
public void addListener(SetChangeListener<? super K> listener) {
changeListeners.add(listener);
}
@Override
public void removeListener(SetChangeListener<? super K> listener) {
changeListeners.remove(listener);
}
//Simple wrapper method that either just pass through to the wrapped set or
//throw an unsupported operation exception
@Override public int size() {return wrappedSet.size();}
@Override public boolean isEmpty() {return wrappedSet.isEmpty();}
@Override public boolean contains(Object o) {return wrappedSet.contains(o);}
@Override public Iterator<K> iterator() {return wrappedSet.iterator();}
@Override public Object[] toArray() {return wrappedSet.toArray();}
@Override public <T> T[] toArray(T[] a) {return wrappedSet.toArray(a);}
@Override public boolean containsAll(Collection<?> c) {return wrappedSet.containsAll(c);}
@Override public boolean add(K e) {throw new UnsupportedOperationException();}
@Override public boolean remove(Object o) {throw new UnsupportedOperationException();}
@Override public boolean addAll(Collection<? extends K> c) {throw new UnsupportedOperationException();}
@Override public boolean retainAll(Collection<?> c) {throw new UnsupportedOperationException();}
@Override public boolean removeAll(Collection<?> c) {throw new UnsupportedOperationException();}
@Override public void clear() {throw new UnsupportedOperationException();}
/**
* Simple implementation of {@link SetChangeListener.Change}
*/
private class BasicSetChange extends SetChangeListener.Change<K> {
/** If true, it is an add change, otherwise it is a remove change*/
private final boolean isAdd;
/** Value that was added or removed */
private final K value;
/**
* @param isAdd {@link #isAdd}
* @param value {@link #value}
*/
public BasicSetChange(boolean isAdd, K value) {
super(ObservableKeySet.this);
this.isAdd = isAdd;
this.value = value;
}
@Override
public boolean wasAdded() {
return isAdd;
}
@Override
public boolean wasRemoved() {
return !isAdd;
}
@Override
public K getElementAdded() {
return isAdd ? value : null;
}
@Override
public K getElementRemoved() {
return isAdd ? null : value;
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Holger |
Solution 2 |