b4kancs
b4kancs

Reputation: 105

JavaFX ListView doesn't refresh properly when removing custom ListCell

I'm working on an editor for my quiz application.

I have a custom ListCell<Question> class that looks like this:

public class QuestionListCell extends ListCell<Question> {

private static final Logger LOGGER = Logger.getLogger(QuizLoader.class.getName());

@FXML HBox cellBox;
@FXML Label title;
@FXML GridPane answersGrid;

private FXMLLoader loader;

@Override
protected void updateItem(Question item, boolean empty) {
    super.updateItem(item, empty);
    if (empty || item == null) {
        setText(null);
        setGraphic(null);
    }
    else {
        if (loader == null) {
            loader = new FXMLLoader(getClass().getResource(QUESTION_LIST_CELL_FXML));
            loader.setController(this);
            try {
                loader.load();
            }
            catch (IOException e) {
                LOGGER.log(Level.SEVERE, e.getMessage(), e);
            }
        }
        addContent(item);
    }
}

private void addContent(Question question) {
    title.textProperty().bind(question.getQuestionTextProperty());

    int answersSize = question.getAnswers().size();
    for (int i = 0; i < answersSize; i++) {
        int rowIndex = i / 2;
        int colIndex = i % 2 == 0 ? 0 : 1;

        Label answerLabel = new Label();
        answerLabel.textProperty().bind(question.getAnswers().get(i).getAnswerTextProperty());
        answerLabel.prefWidthProperty().bind(Bindings.divide(answersGrid.widthProperty(), 2));
        answersGrid.add(answerLabel, colIndex, rowIndex);
    }

    final NumberBinding cellBoxWidth = Bindings.subtract(getListView().widthProperty(), 20);
    cellBox.prefWidthProperty().bind(cellBoxWidth);
    answersGrid.prefWidthProperty().bind(cellBoxWidth);
    setGraphic(cellBox);
}

}

And I set up my ListView in my MainController like this:

@FXML
private void onAddButtonClicked() {
    LOGGER.log(Level.INFO, "addButton clicked.");
    // quiz.getQuestions().add(quiz.getQuestions().get(0));
}

@FXML
private void onDeleteButtonClicked() {
    LOGGER.log(Level.INFO, "deleteButton clicked.");

    int index = listView.getSelectionModel().getSelectedIndex();
    if (index >= 0) {
        quiz.getQuestions().remove(index);
    }
}

private void setUpQuestionsListView() {
    listView.setCellFactory(listView -> new QuestionListCell());
    listView.itemsProperty().bindBidirectional(quiz.getQuestionsProperty());
}

So far so good, the ListView looks like this:

enter image description here

However, when I delete the first (and only the first) item, the GridPane of the second item is seemingly overlaid on the first.

enter image description here

This doesn't happen if I have more elements in the ListView either, as far as I can tell, only if there are but two elements left and I'm deleting the first one. What am I doing wrong?

Upvotes: 1

Views: 763

Answers (2)

Andi Kleinbichler
Andi Kleinbichler

Reputation: 161

If some minor inefficiency and slight performance drawbacks are acceptable, you could also load FXML on each call. If the list is small and the ListCell does not contain much, as it is in your case, there shouldn't be a problem at all. Clearing the children works out well and I did it also in the past. I only observed that it is difficult to understand for others (especially by not so experienced developers) and therefore not unlikely to be broken somewhere in the future.

Here is one of mine examples:

public class ProductItemListCell extends javafx.scene.control.ListCell<ProductModel> {

private Consumer<ProductModel> onDeleteProductCallBack;

ProductItemListCell(Consumer<ProductModel> callback) {
    this.onDeleteProductCallBack = callback;
}

@Override
public void updateItem(ProductModel product, boolean empty)
{
    super.updateItem(product, empty);
    if (empty || product == null) {
        setText(null);
        setGraphic(null);
        return;
    }
    FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("productitem.fxml"));
    try
    {
        fxmlLoader.load();
    }
    catch (IOException e) {
        throw new RuntimeException(e);
    }

    var controller = (ProductItemController)fxmlLoader.getController();
    controller.setProduct(product);
    controller.addListenerForDeleteProduct(this.onDeleteProductCallBack);
    setGraphic(controller.getProductItemBox());
}
}

Upvotes: -1

James_D
James_D

Reputation: 209418

Add the line

answersGrid.getChildren().clear() ;

at the beginning of addContent() (or, if you prefer, in updateItem(), immediately before calling addContent()).

The issue is that you only load the FXML once for each cell (which is a good thing); if (when) the cell is subsequently reused, updateItem() is called again without changing the answersGrid reference, and you add more children to the grid without removing the original ones. Thus when the cell is reused (e.g. through scrolling, or when an item is deleted and its cell is reused for another item), multiple nodes are added to the same locations in the grid.

Removing the existing labels from the grid, as above, will fix the problem.

Upvotes: 3

Related Questions