ListView is not reflecting changes


Question

I created a TableView a while back and registered Properties to each of the TableColumns. Editing of the internal data reflected itself back in the TableView just fine.

With a ListView, however, it is a different story. The changes are not being shown right away unless I close the frame and open it again.

My ListView consists of ActionSteps. Note that I used the Javafx beans properties.

package application.objects;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.function.IntPredicate;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class ActionStep {

   private StringProperty actionStepID;
   private ObjectProperty<LocalDate> dateSet, dateFinished;
   private StringProperty stepName;
   private IntegerProperty completion;
   private ArrayList<StepComment> comments;



   public ActionStep(String name) {
      actionStepID = new SimpleStringProperty();
      stepName = new SimpleStringProperty();
      dateSet = new SimpleObjectProperty<LocalDate>();
      dateFinished = new SimpleObjectProperty<LocalDate>();
      completion = new SimpleIntegerProperty();
      stepName.setValue(name);
   }



   public void setName(String name) {
      stepName.setValue(name);
   }



   public String getName() {
      return stepName.getValue();
   }



   public StringProperty stepNameProperty() {
      return actionStepID;
   }



   public void setID(String id) {
      actionStepID.setValue(id);
   }



   public String getID() {
      return actionStepID.get();
   }



   public StringProperty actionStepIDProperty() {
      return actionStepID;
   }



   public void setCompletion(int percent) {
      if (percent < 0 || percent > 100)
         return;
      completion.set(percent);
   }



   public int getCompletion() {
      return completion.get();
   }



   public IntegerProperty completionProperty() {
      return completion;
   }



   public void setDateSet(LocalDate date) {
      dateSet.set(date);
   }



   public LocalDate getDateSet() {
      return dateSet.get();
   }



   public ObjectProperty<LocalDate> dateSetProperty() {
      return dateSet;
   }



   public void setDateFinished(LocalDate date) {
      dateFinished.set(date);
   }



   public LocalDate getDateFinished() {
      return dateFinished.get();
   }



   public ObjectProperty<LocalDate> dateFinishedProperty() {
      return dateFinished;
   }



   public String toString() {
      return stepNameProperty().get();
   }

}

My ListView uses an ObservableList as well.

  @FXML
  private ListView<ActionStep> actionStepsListView;
  private ObservableList<ActionStep> listOfSteps;

  listOfSteps = FXCollections.observableArrayList();
  actionStepsListView.setItems(listOfSteps);
  if (plan != null) {
     ArrayList<ActionStep> arrayOfSteps = plan.getStepsArrayList();
     for (int i = 0; i < arrayOfSteps.size(); i++)
        listOfSteps.add(arrayOfSteps.get(i));
  } else
     plan = new ActionPlan();

How come changes made to the ObservableList do not reflect themselves in the ListView? I noticed that the ListView called upon every object's toString() to display their values in the ListView, rather than binding it to their Properties.

What am I doing wrong? Am I supposed to override a cell factory or something?

1
3
5/23/2014 6:38:28 AM

Accepted Answer

Note that you're trying to do something more complex with the cells in your ListView than you were with the cells in the TableView. In the TableView, the objects displayed in the cells were changing, so it was easy for the cells to observe this. In the ListView, you want the cells to notice when properties that belong to the objects displayed in the cells change; this is one further step removed, so you have to do a bit of extra coding (though not much, as you'll see).

You could create a custom cell factory to bind to the stepNameProperty(), but it's tricky (you have to make sure to unbind/remove listeners from old items in the updateItem() method).

The easier way, though, which isn't well documented is to use an ObservableList with an extractor defined.

First, fix your method names: you have some weird mismatches in the code you posted. The getX/setX/xProperty method names should all match correctly. I.e. instead of

   public void setName(String name) {
      stepName.setValue(name);
   }



   public String getName() {
      return stepName.getValue();
   }



   public StringProperty stepNameProperty() {
      return actionStepID;
   }

you should have

   public final void setName(String name) {
      stepName.setValue(name);
   }



   public final String getName() {
      return stepName.getValue();
   }


   public StringProperty nameProperty() {
      return stepName;
   }

and similarly for the other property accessor methods. (Obviously, the names of the fields can be anything you like, as they're private.) Making the get/set methods final is good practice.

Then, create the list with an extractor. The extractor is a function that maps each element in the list to an array of Observables which the list will observe. If those values change, it will fire list updates to the list's observers. Since your ActionStep's toString() method references the nameProperty(), I assume you want the ListView to update if the nameProperty() changes. So you want to do

  listOfSteps = FXCollections.observableArrayList(
    actionStep -> new Observable[] { actionStep.nameProperty() } // the "extractor"
  );
  actionStepsListView.setItems(listOfSteps);

Note that in earlier versions of JavaFX 2.2 the ListView did not properly observe the list for update events; this was fixed (if I remember correctly) shortly prior to the release of Java 8. (Since you tagged the question JavaFX8, I assume you're using Java 8 and so you should be fine here.)

If you are not using Java 8, you can use the following (equivalent but more verbose) code:

listOfSteps = FXCollections.observableArrayList(
    new Callback<ActionStep, Observable[]>() {
        @Override
        public Observable[] call(ActionStep actionStep) {
            return new Observable[] { actionStep.nameProperty() } ;
        }
    });
actionStepListView.setItems(listOfSteps);
6
5/23/2014 6:36:29 PM

Here is sample how make listview with custom objects:

public class JavaFX_ListView extends Application {

    class MyObject {
        String day;
        int number;

        MyObject(String d, int n) {
            day = d;
            number = n;
        }

        String getDay() {
            return day;
        }

        int getNumber() {
            return number;
        }

        @Override
        public String toString() {

            return number + " " + day;
        }
    }

    ObservableList<MyObject> myList;

    // Create dummy list of MyObject
    private void prepareMyList() {
        myList = FXCollections.observableArrayList();
        myList.add(new MyObject("Sunday", 50));
        myList.add(new MyObject("Monday", 60));
        myList.add(new MyObject("Tuesday", 20));

    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("sample");
        prepareMyList();
        ListView<MyObject> listView = new ListView<>();
        listView.setItems(myList);

        Pane root = new Pane();
        root.getChildren().add(listView);
        primaryStage.setScene(new Scene(root, 300, 250));
        primaryStage.show();
        // testing
        Timer timer = new Timer();
        timer.schedule(new UpdateListTask(), 1000, 1000);
    }

    public static void main(String[] args) {
        launch(args);
    }

    // testing
    public class UpdateListTask extends TimerTask {
        @Override
        public void run() {
            Platform.runLater(new Runnable() {

                @Override
                public void run() {
                    myList.add(new MyObject("sample", Calendar.getInstance()
                            .getTime().getSeconds()));
                }
            });
        }
    }

}

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon