Issue
I have a problem when I try to bind an element of a list to a JavaFX Label. I don't have the problem if the item is not in a list.
Is the GarbageCollector the reason for this problem?
Controller.class : (controller of the fxml file)
public class Controller implements Initializable {
private final Context context = Context.getInstance();
private List<Label> labels;
public void initialize() {
labels = new ArrayList<>();
for (int i = 0; i < context.getComponents().get(i).size(); i++) {
labels.add(new Label(context.getComponent().get(i).toString()));
labels.get(i).textProperty().bind(context.getComponents().get(i));
gridPane.add(labels.get(i), 0, i); // The GridPane on the FXML
}
}
}
Context.java : (class where the variables are refreshed)
public final class Context implements StatusEventListener, MssEventListener {
private final List<StringProperty> components;
public static Context getInstance() {
return SingletonHandler.instance;
}
private Context() {
super();
components = new ArrayList<>();
for (String component : new InstallProgress().getListInstallingItem()) {
components.add(new SimpleStringProperty(component));
}
}
public List<StringProperty> getComponents() {
return components;
}
// Reset the list
public void setComponents(final List<String> components) {
this.components.clear();
for (String component : components) {
this.components.add(new SimpleStringProperty(component));
}
}
private static class SingletonHandler {
private static final Context instance = new Context();
private SingletonHandler() {
super();
}
}
}
To refresh the label, I use Context.setComponents(<myUpdatedList>)
but nothing happens...
If I refresh a StringProperty component
on Context.java, the display shows the new value of that variable.
Does anyone have an idea to solve this problem?
Thanks in advance
Solution
The bindings bind the text of each label to the corresponding StringProperty
in the context (model). If you replace the entire StringProperty
in the context, the label's text is still bound to the old StringProperty
, not the one that replaced it (you don't update the binding).
(If you don't keep a reference anywhere to the old StringProperty
, it will be eligible for garbage collection. Bindings use weak listeners (which in turn use weak references to the object they are observing) and so your bindings don't prevent the StringProperty
from being garbage collected. This isn't the cause of the problem, though; the issue is simply that your label's text is not bound to the new StringProperty
you create in the setComponents(...)
method.)
Your best option here is to use an ObservableList<String>
in the model class:
public final class Context implements StatusEventListener, MssEventListener {
private final ObservableList<String> components;
public static Context getInstance() {
return SingletonHandler.instance;
}
private Context() {
super();
components = FXCollections.observableArrayList<>(new InstallProgress().getListInstallingItem());
}
public ObservableList<String> getComponents() {
return components;
}
// Reset the list
public void setComponents(final List<String> components) {
this.components.setAll(components);
}
private static class SingletonHandler {
private static final Context instance = new Context();
private SingletonHandler() {
super();
}
}
}
And now your view class can just observe the list. I added handling here for the list changing size, which your original code didn't support:
public class Controller implements Initializable {
private final Context context = Context.getInstance();
private List<Label> labels;
public void initialize() {
labels = new ArrayList<>();
updateLabelsFromContext();
context.getComponents().addListener((Change<? extends String> c) ->
updateLabelsFromContext());
}
private void updateLabelsFromContext() {
List<String> components = context.getComponents();
for (int i=0; i < labels.size() && i < components.size(); i++) {
labels.get(i).setText(components.get(i));
}
// remove excess labels:
for (int i=components.size(); i<labels.size(); i++) {
gridPane.getChildren().remove(labels.get(i));
}
if (components.size() < labels.size()) {
labels.subList(components.size(), labels.size()).clear();
}
// add any new labels needed:
for (int i = labels.size(); i<components.size(); i++) {
Label label = new Label(components.get(i));
labels.add(label);
gridPane.add(label, 0, i);
}
}
}
If it's guaranteed the size of the list in the context is always the same, you can do
public class Controller implements Initializable {
private final Context context = Context.getInstance();
private List<Label> labels; // may not be needed
public void initialize() {
labels = new ArrayList<>();
for (int i = 0; i<context.getComponents().size(); i++) {
Label label = new Label();
labels.add(label); // may not be needed
label.textProperty().bind(Bindings.valueAt(context.getComponents(),i);
gridPane.add(label, 0, i);
}
}
}
Answered By - James_D
Answer Checked By - Candace Johnson (JavaFixing Volunteer)