Fever_Dream
Fever_Dream

Reputation: 13

Issue in ListView in JavaFX

I've been practicing on improving my skills with JavaFX and I faced some issues with the JavaFX ListView. I made a custom layout with a label, circle behaving as an image holder and a checkbox as show down below.

screen capture

Then I created a ListCell with class type 'formation'. The class type contains a string for the label, an image for the circle and a boolean property to keep count of the checkbox state. I tried multiple solutions but I keep facing issues.

First solution was binding the checkbox to the boolean property of the checkbox. The issue is when checking or unchecking a single checkbox all the checkboxes become checked/unchecked.

I also tried a selection listener for the checkbox but the when setting the initial state of the checkbox programmatically, in the ListView, it interferes with the listener (keeps triggering the listener and make uncalled changes).

what i want is to make the change made to a specific checkbox to not affect all checkboxes and make the checkbox selection listener only respond to user generated clicks (checking the checkbox by clicking) and not programmatic changes (ex checkbox.setselected(true))

This is the code for the ListCell:

package Formation.cells;

import Formation.Formation;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.embed.swing.SwingFXUtils;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;

import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean;

public class Available_formation_cell extends ListCell<Formation> {
    private AnchorPane layout;
    private Label formation_name;
    private Circle formation_icon;

    private JFXCheckBox select_box;

    public Available_formation_cell(){
    try{
        FXMLLoader load_available_formations_layout=new FXMLLoader(getClass().getResource("/Formation/available_formation_layout.fxml"));
        layout=load_available_formations_layout.load();
        this.formation_name= (Label) load_available_formations_layout.getNamespace().get("formation_name");
        this.formation_icon =(Circle) load_available_formations_layout.getNamespace().get("formation_icon");
        this.select_box=(JFXCheckBox) load_available_formations_layout.getNamespace().get("select_box");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    protected void updateItem(Formation formation, boolean b) {
        super.updateItem(formation, b);
        select_box.selectedProperty().unbind();
        if(formation==null || b){
            setGraphic(null);
            setText(null);
        }
        else{
            formation_name.setText(formation.getFormation_name());
            Image formation_image=formation.getFormation_image();

       
                formation_icon.setFill(new ImagePattern(formation_image));
            
            if(formation.isSelected_available_group()){
                select_box.setSelected(true);
            }
            else{
                select_box.setSelected(false);
            }
                select_box.selectedProperty().bindBidirectional(formation.selected_available_groupProperty());

            setGraphic(layout);
            setText(null);
        }
    }
}

Upvotes: 1

Views: 170

Answers (1)

Slaw
Slaw

Reputation: 46170

The Problem

You are bidirectionally binding the check box's selected property, but are calling unbind() in an attempt to unbind it. That method can only be used to unbind a unidirectional binding. That means you are never actually unbinding the check box's selected property from the item's selected property. On top of that, cells are reused and multiple bidirectional bindings can exist simultaneously for the same property. Ultimately, you end up with every check box bound to multiple items, meaning an update to one will propagate to all.


Solutions

There are at least two solutions here.

Properly Unbind the Bidirectional Binding

To unbind a bidirectional binding, you must call unbindBidirectional(Property). This will unfortunately make your code a little more complex, as you'll have to maintain a reference to the old Property in order to unbind for it. Luckily, with how cells and their updateItem method work, you can get the old item by calling getItem() before calling super.updateItem(...).

Here's an example.

Formation.java

import javafx.beans.property.*;
import javafx.scene.image.Image;

public class Formation {

    private final StringProperty name = new SimpleStringProperty(this, "name");
    public final void setName(String name) { this.name.set(name); }
    public final String getName() { return name.get(); }
    public final StringProperty nameProperty() { return name; }

    private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
    public final void setImage(Image image) { this.image.set(image); }
    public final Image getImage() { return image.get(); }
    public final ObjectProperty<Image> imageProperty() { return image; }

    private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
    public final void setSelected(boolean selected) { this.selected.set(selected); }
    public final boolean isSelected() { return selected.get(); }
    public final BooleanProperty selectedProperty() { return selected; }
}

FormationListCell.java

import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;

public class FormationListCell extends ListCell<Formation> {

    private VBox container;
    private Label label;
    private Circle circle;
    private CheckBox checkBox;

    @Override
    protected void updateItem(Formation item, boolean empty) {
        Formation oldItem = getItem();

        super.updateItem(item, empty);

        if (empty || item == null) {
            setText(null);
            setGraphic(null);
            if (oldItem != null) {
                unbindState(oldItem);
            }
        } else if (oldItem != item) {
            if (container == null) {
                createGraphic();
            } else if (oldItem != null) {
                unbindState(oldItem);
            }
            setGraphic(container);

            label.textProperty().bind(item.nameProperty());
            circle.fillProperty().bind(Bindings.createObjectBinding(() -> {
                Image image = item.getImage();
                return image == null ? null : new ImagePattern(image);
            }, item.imageProperty()));
            checkBox.selectedProperty().bindBidirectional(item.selectedProperty());
        }
    }

    private void unbindState(Formation oldItem) {
        label.textProperty().unbind();
        label.setText(null); // don't keep a strong reference to the string

        circle.fillProperty().unbind();
        circle.setFill(null); // don't keep a strong reference to the image

        // correctly unbind the check box's selected property
        checkBox.selectedProperty().unbindBidirectional(oldItem.selectedProperty());
    }

    private void createGraphic() {
        label = new Label();

        circle = new Circle(25, null);
        circle.setStroke(Color.BLACK);

        checkBox = new CheckBox();

        container = new VBox(10, label, circle, checkBox);
        container.setAlignment(Pos.TOP_CENTER);
    }
}

Main.java

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<Formation> listView = new ListView<>();
        listView.setCellFactory(lv -> new FormationListCell());
        for (int i = 1; i <= 1000; i++) {
            Formation formation = new Formation();
            formation.setName("Formation " + i);
            formation.setSelected(Math.random() < 0.5);
            listView.getItems().add(formation);
        }

        primaryStage.setScene(new Scene(listView, 600, 400));
        primaryStage.show();
    }
}

Use a Listener Instead of Bindings

Instead of using bindings, you can instead just set the UI state in the updateItem method directly. Then have a listener on the check box's selected property that updates the item as appropriate. However, in order to have the UI stay in sync with the Formation items when their properties are changed elsewhere, you'll need to set the list view's items to an ObservableList that was created with a so-called "extractor". The extractor allows the list to observe properties of its elements, ultimately meaning the updateItem method of the cell will be invoked when any item's property is invalidated.

Personally, I prefer the binding solution, because then it "just works" after setting the cell factory of the list view, whereas this approach requires you also remember to set the items property to a custom list.

Here's an example.

Formation.java

Unchanged from previous solution.

FormationListCell.java

import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;

public class FormationListCell extends ListCell<Formation> {

    private VBox container;
    private Label label;
    private Circle circle;
    private CheckBox checkBox;

    @Override
    protected void updateItem(Formation item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            setText(null);
            setGraphic(null);
            if (container != null) {
                // no need to "clear" check box's selected property
                label.setText(null);
                circle.setFill(null);
            }
        } else {
            if (container == null) {
                createGraphic();
            }
            setGraphic(container);

            label.setText(item.getName());
            circle.setFill(item.getImage() == null ? null : new ImagePattern(item.getImage()));
            checkBox.setSelected(item.isSelected());
        }
    }

    private void createGraphic() {
        label = new Label();

        circle = new Circle(25, null);
        circle.setStroke(Color.BLACK);

        checkBox = new CheckBox();
        // have a listener update the model property
        checkBox.selectedProperty().addListener((obs, oldValue, newValue) -> {
            Formation item = getItem();
            if (item != null && newValue != item.isSelected()) {
                item.setSelected(newValue);
            }
        });
        
        container = new VBox(10, label, circle, checkBox);
        container.setAlignment(Pos.TOP_CENTER);
    }
}

Main.java

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import javafx.util.Callback;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<Formation> listView = new ListView<>();
        listView.setItems(FXCollections.observableArrayList(formationPropertyExtractor()));
        listView.setCellFactory(lv -> new FormationListCell());
        for (int i = 1; i <= 1000; i++) {
            Formation formation = new Formation();
            formation.setName("Formation " + i);
            formation.setSelected(Math.random() < 0.5);
            listView.getItems().add(formation);
        }

        primaryStage.setScene(new Scene(listView, 600, 400));
        primaryStage.show();
    }

    private Callback<Formation, Observable[]> formationPropertyExtractor() {
        return f -> new Observable[] {f.nameProperty(), f.imageProperty(), f.selectedProperty()};
    }
}

Upvotes: 5

Related Questions