9

I have a ListView control in a JavaFX 2 modal dialog window.

This ListView displays DXAlias instances, the ListCells for which are manufactured by a cell factory. The main thing the factory objects do is examine the UserData property data of the ListView and compare it to the item corresponding to the ListCell. If they are the same, the contents of the ListCell are rendered in red, otherwise black. I do this to indicate which of the items in the ListView is currently selected as the "default". Here is my ListCell factory class so you can see what I mean:

private class AliasListCellFactory implements 
    Callback<ListView<DXSynonym>, ListCell<DXSynonym>> {

@Override
public ListCell<DXSynonym> call(ListView<DXSynonym> p) {
    return new ListCell<DXSynonym>() {

    @Override
    protected void updateItem(DXSynonym item, boolean empty) {
        super.updateItem(item, empty);

        if (item != null) {
            DXSynonym dx = (DXSynonym) lsvAlias.getUserData();

            if (dx != null && dx == item) {
                this.setStyle("-fx-text-fill: crimson;");                    
            } else { this.setStyle("-fx-text-fill: black;"); }

            this.setText(item.getDxName());

        } else { this.setText(Census.FORMAT_TEXT_NULL); }
    }};
}

I have a button handler called "handleAliasDefault()" which makes the selected item in the ListView the new default by taking the selected DXAlias instance and storing it into the ListView: lsvAlias.setUserData( selected DXAlias ). Here is the handler code:

// Handler for Button[fx:id="btnAliasDefault"] onAction
    @FXML
    void handleAliasDefault(ActionEvent event) {

        int sel = lsvAlias.getSelectionModel().getSelectedIndex();
        if (sel >= 0 && sel < lsvAlias.getItems().size()) {
            lsvAlias.setUserData(lsvAlias.getItems().get(sel));
        }
    }

Because the change that is made in response to clicking on the Set Default button is to change the ListView's UserData() without any change to the backing ObservableList, the list does not correctly indicate the new default.

Is there a way to force a ListView to re-render its ListCells? There are a quadrillion questions from Android users on this subject, but there appears to be no happiness for JavaFX. I may have to make a "meaningless change" to the backing array to force a redraw.

I see that this was asked for JavaFX 2.1: Javafx ListView refreshing

Community
  • 1
  • 1
scottb
  • 9,908
  • 3
  • 40
  • 56

5 Answers5

16

For any body else that ends up on this page:

The correct way of doing this is to supply your observable list with an "extractor" Callback. This will signal the list (and ListView) of any property changes.

Custom class to display:

public class Custom {
    StringProperty name = new SimpleStringProperty();
    IntegerProperty id = new SimpleIntegerProperty();

    public static Callback<Custom, Observable[]> extractor() {
        return new Callback<Custom, Observable[]>() {
            @Override
            public Observable[] call(Custom param) {
                return new Observable[]{param.id, param.name};
            }
        };
    }

    @Override
    public String toString() {
        return String.format("%s: %s", name.get(), id.get());
    }
}

And your main code body:

ListView<Custom> myListView;
//...init the ListView appropriately
ObservableList<Custom> items = FXCollections.observableArrayList(Custom.extractor());
myListView.setItems(items);
Custom item = new Custom();
items.add(item);
item.name.set("Mickey Mouse");
// ^ Should update your ListView!!!

See (and similar methods): https://docs.oracle.com/javafx/2/api/javafx/collections/FXCollections.html#observableArrayList(javafx.util.Callback)

Bryan Pugh
  • 113
  • 1
  • 7
Andrey
  • 830
  • 9
  • 12
  • could you explain how the usage of SimpleIntegerProperty is used in your example – serup Jun 22 '16 at 09:13
  • 1
    @serup I'm not entirely sure what your question is, but let me try explain. In my example, I never explicitly set the value of the id, so it would default to 0. This would result in the ListView the row to display "Mickey Mouse: 0". If you wanted to set the ID value, you could use `item.name.set(6)`, for example, and you would see the ListValue update correctly. Depending on your use-case, using a non-default constructor on the `Custom` class would probably be "better". – Andrey Jun 23 '16 at 16:58
  • @Andrey is there a similar solution for treeView? – Hosseinmp76 Jul 30 '20 at 13:22
  • This solution should in most cases be preferred over the solution from @Eldelshell. That is because the `ObservableList` containing the items could be used in multiple `ListView` instances. It is unwieldy to keep track of this and call `refresh` on all those instances. Also, the items of a ListView could be replaced by another ObservableList, so doing a refresh when an item has changed, but the item is not in the ListView anymore is a bit wasteful. – Leo Mekenkamp Apr 03 '21 at 14:34
13

Not sure if this works in JavaFX 2.2, but it does in JavaFX 8 and took me a while to figure out. You need to create your own ListViewSkin and add a refresh method like:

public void refresh() {
    super.flow.recreateCells(); 
}

This will call updateItem without having to replace the whole Observable collection.

Also, to use the new Skin, you need to instantiate it and set it on the initialize method of the controller in case you're using FXML:

MySkin<Subscription> skin = new MySkin<>(this.listView); // Injected by FXML
this.listView.setSkin(skin);
...
((MySkin) listView.getSkin()).refresh(); // This is how you use it    

I found this after debugging the behavior of an Accordion. This controls refresh the ListView it contains everytime you expand it.

aboger
  • 2,214
  • 6
  • 33
  • 47
Eldelshell
  • 6,683
  • 7
  • 44
  • 63
  • 1
    Here's how this is used on one of my OSS projects: https://github.com/Eldelshell/JobHunter/blob/master/jobhunter/src/main/java/jobhunter/gui/UpdateableListViewSkin.java – Eldelshell Feb 23 '16 at 10:35
9

I am not sure if I am missing something, but at first I tried scottb's solution, but then I found out that there is an already implemented refresh() method, which did the trick for me.

ListView<T> list;
list.refresh();
Dom
  • 1,427
  • 1
  • 12
  • 16
5

For now, I am able to get the ListView to redraw and correctly indicate the selected default by using the following method, called forceListRefreshOn(), in my button handler:

@FXML
void handleAliasDefault(ActionEvent event) {

    int sel = lsvAlias.getSelectionModel().getSelectedIndex();
    if (sel >= 0 && sel < lsvAlias.getItems().size()) {
        lsvAlias.setUserData(lsvAlias.getItems().get(sel));
        this.<DXSynonym>forceListRefreshOn(lsvAlias);
    }
}

The helper method just swaps out the ObservableList from the ListView and then swaps it back in, presumably forcing the ListView to update its ListCells:

private <T> void forceListRefreshOn(ListView<T> lsv) {
    ObservableList<T> items = lsv.<T>getItems();
    lsv.<T>setItems(null);
    lsv.<T>setItems(items);
}
scottb
  • 9,908
  • 3
  • 40
  • 56
0

Seems, you don't need to use updateItem method. What you need - is to store indicator of currently default cell (which is in user data now), into OblectProperty. And add a listener on this property from each cell. When value changes, reassign a style.

But I think, such binding will call another problem - while scrolling, new cells will be created, but the old ones will not leave the binding, which can cause memory leak. So you need to add listener on cell removing from the scene. I think, it can be done by adding a listener on the parentProperty, when it becomes null - remove binding.

UI shouldn't be forced to be updated. It is updated automaticly, when properties/rendering of existing nodes is changed. So you just need to update the appearance/property of existing nodes (cells). And not to forget, that cells can be created massively, during scrolling/rerendering, etc.

Alexander Kirov
  • 3,624
  • 1
  • 19
  • 23
  • I didn't show the code, but the data for the ListView is in an ObservableList. Each of the data model items is a JavaFX property. The contract for the ListView says that when the data changes, the view is automatically updated. It doesn't appear to say anything about what should happen when the way in which the data is displayed changes when the data itself does not. Adding a new binding may provide a solution ... but at the expense of a lot of very ugly and, perhaps, fragile code. All I need is for the ListCells to be redrawn when the selected default changes. – scottb Jun 02 '13 at 16:07
  • This is common property of how javafx 2 works: when node of scenegraph (particular cell) changes its properties, so that it affects on visual representation, scenegraph redraws those nodes (cells) if needed. So, when you update visual appearance of cells - they will be redrawn by scenegraph. – Alexander Kirov Jun 02 '13 at 16:11
  • I see what you're saying ... but I also don't think that I should be forced to refactor my data model in order to include a property that isn't actually a part of my data model. I just want the selected default alias to be rendered in red, instead of black. My problem would be fixed if there were one simple method available to me: lsvAlias.requestRefresh(); – scottb Jun 02 '13 at 16:18
  • Ok, then, you can file a RFE to the javafx-jira =) – Alexander Kirov Jun 02 '13 at 16:21
  • For now, I can probably kludge a work-around by adding and then removing an item from the backing ObservableList. I expect that this would force the ListView to be redrawn. I'll give it a try ... if it works, it would be much less ugly than binding a new property to all my ListCells. – scottb Jun 02 '13 at 16:25
  • The last suggestion : Parent.requestLayout() - doesn't help? – Alexander Kirov Jun 02 '13 at 16:28
  • I tried lsvAlias.requestLayout() ... but the ListView is in an AnchorPane. I'll try requestLayout() on that and see. Thanks for the tip. – scottb Jun 02 '13 at 16:30
  • No effect of lsvAlias.Parent.requestLayout(); ... a helper method called forceListRefreshOn(lsvAlias); worked tho. – scottb Jun 02 '13 at 16:35