6

I'm using a ListView in a JavaFX application. The items in the list require more than just a string to display them so I made a custom implementation of ListCell<T>, where T is the class of the objects I'm displaying. In this custom class, I create a BorderPane in the constructor and override updateItem(T item, boolean empty). The idea was that when updateItem is called, I set the graphic of the cell to be the BorderPane while change some properties of Labels inside of it. However, I noticed updateItem is not called once (e.g. when a cell is recycled through scrolling or a new item is added), but it is called all the time, e.g. when the focus changes from one cell to another (even without scrolling), or when the scene is resized, or when a button inside the borderpane is pressed, ...

I need to have a ListView with custom ListCells. I want to receive one callback when a cell is reused, passing the new item to be rendered as a parameter. Then I want to use that item as a model to construct a view-controller pair of which I take the view and use it as the graphic of the cell. Buttons and other controls inside this view should work. Is this possible in JavaFX?

Linked problem:

Community
  • 1
  • 1
RDM
  • 4,986
  • 4
  • 34
  • 43

2 Answers2

10

The basic idea is that cells are constructed rarely but the updateItem(...) method is likely to be called frequently. There is no actual guarantee that the item has really changed between calls to updateItem(...). The default implementation of updateItem(...) takes care of handling selection, focus, etc, so this method probably needs to be invoked if any of those properties change (even if the item has not changed).

You should therefore strive to reduce the overhead of the updateItem(...) method. It's not clear that multiple, frequent calls would prevent you doing what you want (for example, when you pass the new item as a parameter to your model, check to see if it's really different to the one you already have before doing any updates).

I'd argue that updateItem(...) is really somewhat misnamed: it's called not only when the item is updated, but really any time the cell might need to be updated. There's already a mechanism for executing code only when the item changes though: just register a listener with the cell's itemProperty(). You can use this technique (which I generally prefer) to create a different style of ListCell:

ListView<...> listView = ... ;
listView.setCellFactory(lv -> {
    BorderPane cellRoot = new BorderPane();
    // create nodes, register listeners on them, populate cellRoot, etc...
    ListCell<...> cell = new ListCell<>();
    cell.itemProperty().addListener((obs, oldItem, newItem) -> {
        if (newItem != null) {
            // update cellRoot (or its child nodes' properties) accordingly
        }
    });
    cell.emptyProperty().addListener((obs, wasEmpty, isEmpty) -> {
        if (isEmpty) {
            cell.setGraphic(null);
        } else {
            cell.setGraphic(cellRoot);
        }
    });
    cell.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    return cell ;
});

This approach may work better in your scenario.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • 1
    I've solved my current issue by using a ScrollPane and custom BorderPanes to emulate the list cells. I suppose your solution could work, I will test this when I have more time. However, I think the way updateItem works according to the API is really shit. Why would there not be one function that is called once per item update? :( – RDM Aug 13 '14 at 09:02
  • My solution **does** work, and demonstrates precisely how to write a callback that is called only when the item changes. The purpose of the API is to allow for efficient `ListView`s that might contain millions of items (should that ever be needed). In order to do that, there needs to be a callback that is called whenever the presentation of the cell might change, rather than when the item changes. `updateItem(...)` is that method. – James_D Aug 13 '14 at 12:28
  • 1
    @Warkst You are not the only one who doesn't like how `updateItem` works. I created [Flowless](https://github.com/TomasMikula/Flowless) to address the problem with too many `updateItem` calls. You can use it instead of your workaround. Flowless does not force you to reuse cells, which IMO results in simpler usage. The implementation is still efficient in the sense that only visible cells are created and attached to the scene graph. – Tomas Mikula Aug 23 '14 at 15:43
  • @Warkst Kudos to the statement "However, I think the way updateItem works according to the API is really shit". The mechanism should be changed to a rather sensible one. – Ishrak Aug 02 '17 at 13:15
  • @James_D I am trying this solution and I am seeing that the callback of `itemProperty` is being called multiple times in a row and always with `oldItem` as null and `newItem` with the same item. What is triggering the listener is that `setItem` in `super.updateItem` is invoked – IS1_SO May 29 '18 at 16:24
  • @IS1_SO Presumably you must have multiple different cells with the same value then...?\ – James_D May 29 '18 at 16:26
  • @James_D No. Only one cell appears in the UI – IS1_SO May 29 '18 at 16:27
  • @IS1_SO Not sure how that is possible... Typically though the list view will create some additional cells and cache them, so you may have a few more cells initialized than are actually visible. Suggest you post a new question with a [MCVE] if you think it is not behaving as expected. – James_D May 29 '18 at 16:29
  • @James_D Analyzing a bit further I am seeing that the itemProperty callback is invoked with `old=null` and `new=`. Then it is `old=` and `new=null`. Finally again `old=null` and `new=`. Is this the expected behavior even though there's only 1 item? – IS1_SO May 29 '18 at 19:55
  • @IS1_SO Depends on what you "expect" :). In the sense that the listener is only being invoked when the item actually changes, then yes. As to why the `ListView` appears to initialize the cell's items, then null them out, and then reinitialize them when the list view is first created, I cannot explain why they did that. (Best guess would be something to do with initially computing the list view's preferred size, or something.) I think after this, the listener is only invoked when you expect (scrolling, changing size, or when the item actually changes, etc). – James_D May 29 '18 at 19:59
0

I'm just starting with JavaFX and I won't be able to explain all the little details for why it worked (it calls the method on new and existing items), but the way I solved the problem is by simply calling myContainer.getChildren().removeAll( ... )

In my implementation of ListCell public class MyCell extends ListCell<MyContent> I'm creating many different items and adding them to a container, which becomes a line in my list view. So, I just had to remove the same items from that container right before I add them back in. Here's simplified version of what I've done. Seems to be working just fine. I did try James_D's suggestion, but it didn't work for me and produced same errors.

@Override
    public void updateItem(MyContent item, boolean empty) {

        super.updateItem(item, empty);

        if (item == null || empty == true) {
            setGraphic(null);
            setText(null);
        } else {    

            // here I user the "item" to decide how to create 
            // objects a, b, c ... which are Rectangle, Label and Label
            // I also update myMainContainer, which is a Pane

            myMainContainer.getChildren().removeAll(a, b, c);
            myMainContainer.getChildren().addAll(a, b, c);
            setGraphic(myMainContainer);
        }
    }