0

My problem is that my ListView (checkbox list view) does not update when the ObservableList changes.

This problem only happens when I use a dated version of JavaFX and a dated JRE (jre6 and jfx2.2). When I use jre8 and the JavaFX that it includes, the problem is gone and the list refreshes nicely.

Working source code on latest JavaFX described above:

The list:

public ObservableList<TestCaseCheckboxModel> TestCases;

//Constructor
public SessionTestCaseModel() {
    TestCases = FXCollections.observableArrayList(new Callback<TestCaseCheckboxModel, Observable[]>(){
        @Override
        public Observable[] call(TestCaseCheckboxModel model) {
            return new Observable[]{model.TestCaseName, model.TestCaseStatus, model.TestCaseSelected};//selected might not be needed since it's bound to the listview anyways
        }
    });
}

The Class the list uses has these properties:

public StringProperty TestCaseName = new SimpleStringProperty();
public IntegerProperty TestCaseStatus = new SimpleIntegerProperty();
public BooleanProperty TestCaseSelected = new SimpleBooleanProperty();

The List is populated externally.

The ListView:

@FXML
private ListView<TestCaseCheckboxModel> testcaseListView;

//bind the checkbox select to the model property
testcaseListView.setCellFactory(CheckBoxListCell.forListView(new Callback<TestCaseCheckboxModel, ObservableValue<Boolean>>() {
   @Override
   public ObservableValue<Boolean> call(TestCaseCheckboxModel item) {
        return item.TestCaseSelected;
    }
}));

//initiate the checkbox list view with the model items
testcaseListView.setItems(getModel().TestCases);

The problem is that when I change the List items, the ListView won't change on the old JavaFX version.

It might be worth mentioning that after I add/remove an item from the list, the items that are in the list update (like they are supposed to when they themselves change). But, as far as I know, calling the add function of the list triggers the ListView update, no matter what.

On the other hand, when the item properties change, this will trigger, with both the old and new JavaFX versions, so the Callback extractor is working as intended:

getModel().TestCases.addListener(new ListChangeListener<TestCaseCheckboxModel>(){
         @Override
            public void onChanged(Change change) {
                System.out.println(change);
        }
    });

Is there a known workaround for this?

Thank you for your help.

nmarton.t
  • 15
  • 5
  • Could you post an example code modifying the data? Furthermore, **if** you make those property fields `public`, you should also add the `final` modifier, just to be sure the properties are not replaced. – fabian Aug 17 '16 at 13:11
  • Thank you for the suggestion, I will definitely include safety measures, but I rather wanted to get this problem out of my way first. An example could simply be `getModel().TestCases.get(0).TestCaseName.set("New name")`. Like this, any of the properties can be modified from the `TestCases` list. So there is no special trick for that. – nmarton.t Aug 17 '16 at 13:54
  • And make sure to check the answer that I posted that solves the issue, if you were wondering if the error was in modifying the data. It was not, and I found the solution as well (answer below). I will accept it as soon as I can (day after tomorrow) so it will be obvious. – nmarton.t Aug 17 '16 at 13:59

2 Answers2

1

Answering my own question with help from this thread: https://stackoverflow.com/a/25962110/4073727

I implemented a custom ListViewSkin that is capable of updating the ListView "from the inside":

public class UpdateableListViewSkin<T> extends ListViewSkin<T> {

    public UpdateableListViewSkin(ListView<T> arg0) {
        super(arg0);
    }

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

}

Then I add the ObservableList to the ListView, instantiate the Skin and set it to the ListView.

//initiate the checkbox list view with the model items
testcaseListView.setItems(getModel().TestCases);

UpdateableListViewSkin<TestCaseCheckboxModel> skin = new UpdateableListViewSkin<TestCaseCheckboxModel>(testcaseListView);
testcaseListView.setSkin(skin);

The key is that you need to add a working onChange listener to the ObservableList, which will trigger the Skin's .refresh() method. I did this right after I set the Skin to the ListView:

getModel().TestCases.addListener(new ListChangeListener<TestCaseCheckboxModel>(){
    @SuppressWarnings("unchecked")
    @Override
        public void onChanged(Change change) {
            ((UpdateableListViewSkin<TestCaseCheckboxModel>)testcaseListView.getSkin()).refresh();
        }
    });

This workaround triggers the ListView's update functions, like it does on the new version of JavaFX.

Community
  • 1
  • 1
nmarton.t
  • 15
  • 5
  • The problem you might run into here is that in Java 9, `ListViewSkin` will change package, so this workaround will fail to even run in that release. Do you really still need to support Java 6? – James_D Aug 17 '16 at 16:37
  • Yes, I absolutely do, the application will have to run on dated JVMs (old MATLAB versions use jre6 so that is my requirement). Thank you for the heads up, though, I will keep that in mind and find a fix for it when the problem arises. – nmarton.t Aug 18 '16 at 06:52
0
Model - Contact
UI- ListView
Below is the controller


package nmmu.wrap301.jfx01;

import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.util.Callback;

import java.util.ArrayList;

public class Controller {
    // List of contacts in an UNOBSERVED list.
    private ArrayList<Contact> contacts = new ArrayList<Contact>();

    // Observable List needed by the ListView to know when to refresh itself (i.e. when adding or removing
    // items from the list being displayed).
    private ObservableList<Contact> observableContacts;

    public Controller() {
        setupContacts();
    }

    /**
     * Add some dummy contacts
     */
    private void setupContacts() {
        System.out.println("Adding contacts.");

        contacts.add(new Contact("Billy Bob", "081-556-1234"));
        contacts.add(new Contact("Jane Doe", "082-689-2546"));
        contacts.add(new Contact("Joe Soap", "084-253-1254"));
    }

    // region Cached references to scene's controls
    private TextField txtName;
    private TextField txtContactNumber;
    private Button btnNew;
    private ListView<Contact> lbxContacts;
    // endregion

    /**
     * Bind the UI controls and the controller properties together.
     * @param scene The scene being bound too.
     */
    public void connectToUI(Scene scene)
    {
        // region Obtain cached references to controls for easy use later.
        System.out.println("Obtaining references to scene controls by id.");

        txtName = (TextField) scene.lookup("#name");
        txtContactNumber = (TextField) scene.lookup("#number");
        btnNew = (Button) scene.lookup("#new");
        lbxContacts = (ListView) scene.lookup("#contacts");
        // endregion

        // region Set up the objects to be viewed, as well as WHEN the ListView will be refreshed.
        /*
            Create an extractor that provides the ListView with an array of properties that
            would cause the toString method to display something else, i.e. the ListView will watch
            these properties to decide when to refresh itself.

            The extractor is given an object when you ADD something to the ListView collection it is
            displaying. Given the object, the extractor simply returns an array to properties. The ListView
            will attach listeners to these properties. When the object is REMOVED from the collection,
            the ListView will stop watching the properties.
         */
        Callback<Contact, Observable[]> extractor = CONTACT -> {
            return new Observable[] {CONTACT.contactNumberProperty(), CONTACT.nameProperty()};
        };

        /*
            Create a new ObservableArrayList (to the ListView is told WHEN objects are added and removed).
            An extractor is provided IF you want the ListView to refresh if the contents of the object
            are changed (i.e. a contact's name or number).
         */
        observableContacts = FXCollections.observableArrayList(extractor);

        // Add some dummy contacts. Note adding contents of an unobserved list to the observed list.
        observableContacts.addAll(contacts);

        // Tell the ListView WHAT it is to display.
        lbxContacts.setItems(observableContacts);
        // endregion

        // region When select item in ListView, bind/unbind properties of the TextFields
        lbxContacts.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            System.out.printf("old = '%s', new = '%s'\n", oldValue, newValue);

            // remove the old binding if it exists
            if (oldValue != null) {
                System.out.printf("unbinding from '%s', ", oldValue);
                txtName.textProperty().unbindBidirectional(oldValue.nameProperty());
                txtContactNumber.textProperty().unbindBidirectional(oldValue.contactNumberProperty());
            }

            // if the new value is not null
            if (newValue != null) {
                // bind to the new one
                System.out.printf("binding to '%s'\n", newValue);
                txtName.textProperty().bindBidirectional(newValue.nameProperty());
                txtContactNumber.textProperty().bindBidirectional(newValue.contactNumberProperty());
            }
        });
        // endregion

        // region Add event handler for New Button.
        btnNew.setOnAction(event -> {
            Contact contact = new Contact("?", "000-000-0000");
            lbxContacts.getItems().add(contact);
            lbxContacts.getSelectionModel().selectLast();
        });
        // endregion

        // Start off by selecting the first contact
        lbxContacts.getSelectionModel().selectFirst();
    }
}

A tableview, kind of simple

package nmu.wrpv301.jfx03b;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;

public class Controller {
    private ObservableList<Contact> contacts;

    // region Cached references.
    private Scene scene;
    private TableView<Contact> tblContacts;
    private Button btnNew;
    private Button btnModify;
    private Button btnDelete;
    private CheckBox cbxEditable;

    private TableColumn colFirstName;
    private TableColumn colSurname;
    private TableColumn colContactNumber;
    // endregion

    public Controller(Scene scene) {
        this.scene = scene;

        setupContacts();
        cacheReferences();
        setupTable();
        setTableEditable();
        attachEventHandlers();
    }

    private void setupContacts() {
        // Create the observable list.
        contacts = FXCollections.observableArrayList();

        // Set extractor?

        // Add some dummy values.
        contacts.addAll(
                new Contact("Joe", "Soap", "1234-5678-987"),
                new Contact("Jane", "Doe", "2345-6857-865"),
                new Contact("Billy", "Bob", "9856-8547-521"));
    }

    private void cacheReferences() {
        tblContacts = (TableView<Contact>) scene.lookup("#table");
        btnNew = (Button) scene.lookup("#new");
        btnModify = (Button) scene.lookup("#modify");
        btnDelete = (Button) scene.lookup("#delete");
        cbxEditable = (CheckBox) scene.lookup("#editable");
    }

    private void setupTable() {
        // region Create a column PER property you want displayed in the TableView.
        colFirstName = new TableColumn("First Name");
        colFirstName.setCellValueFactory(
                new PropertyValueFactory<Contact, String>("firstName"));

        colSurname = new TableColumn("Surname");
        colSurname.setCellValueFactory(
                new PropertyValueFactory<Contact, String>("surname"));

        colContactNumber = new TableColumn("Contact Number");
        colContactNumber.setCellValueFactory(
                new PropertyValueFactory<Contact, String>("contactNumber"));
        // endregion

        // Now add all the columns to the TableView.
        tblContacts.getColumns().addAll(colSurname, colFirstName, colContactNumber);

        // Set the list the TableView is to display.
        tblContacts.setItems(contacts);
    }

    private void setTableEditable() {
        // region Indicate PER column, what control to use and how to set the value.
        colSurname.setCellFactory(TextFieldTableCell.forTableColumn());
        colSurname.setOnEditCommit(event -> {
            // Typecast event into something more usable.
            TableColumn.CellEditEvent<Contact, String> e = (TableColumn.CellEditEvent<Contact, String>) event;

            // Get specific contact that was being edited.
            Contact contact = e.getTableView().getSelectionModel().getSelectedItem();
            // Change its surname to that in the TextField.
            contact.surnameProperty().set(e.getNewValue());
        });

        colFirstName.setCellFactory(TextFieldTableCell.forTableColumn());
        colFirstName.setOnEditCommit(event -> {
            // Typecast event into something more usable.
            TableColumn.CellEditEvent<Contact, String> e = (TableColumn.CellEditEvent<Contact, String>) event;

            // Get specific contact that was being edited.
            Contact contact = e.getTableView().getSelectionModel().getSelectedItem();
            // Change its surname to that in the TextField.
            contact.firstNameProperty().set(e.getNewValue());
        });

        colContactNumber.setCellFactory(TextFieldTableCell.forTableColumn());
        colContactNumber.setOnEditCommit(event -> {
            // Typecast event into something more usable.
            TableColumn.CellEditEvent<Contact, String> e = (TableColumn.CellEditEvent<Contact, String>) event;

            // Get specific contact that was being edited.
            Contact contact = e.getTableView().getSelectionModel().getSelectedItem();
            // Change its surname to that in the TextField.
            contact.contactNumberProperty().set(e.getNewValue());
        });
        // endregion

        // Initially not editable until checkbox checked.
        tblContacts.setEditable(false);
    }

    private void attachEventHandlers() {
        btnNew.setOnAction(event ->
                contacts.add(new Contact("?","?","?")));

        btnModify.setOnAction(event ->
                contacts.get(0).surnameProperty().set("Whodis"));

        btnDelete.setOnAction(event ->
                contacts.remove(tblContacts.getSelectionModel().getSelectedItem()));

        cbxEditable.setOnAction(event -> tblContacts.setEditable(cbxEditable.isSelected()));

        tblContacts.getSelectionModel().selectedItemProperty().addListener((observable,oldValue,newValue) ->
            System.out.printf("Contact selected = %s\n", newValue));
    }
}