Search code examples
javajavafxobserver-pattern

Binding an element of a list to a Label


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);
        }        
        
      }
    
    
    }