web-dev-qa-db-fra.com

JavaFX: Mise à jour de ListView si un élément de ObservableList change

Je souhaite afficher une liste de personnes (codée dans POJOS et contenant une propriété name et name) à l'aide d'un contrôle JavaFX ListView. J'ai créé le ListView et ajouté la liste des personnes en tant que ObservableList. Tout fonctionne correctement si je supprime ou ajoute une nouvelle personne à ObservableList, mais les modifications apportées au POJO ne déclenchent pas une mise à jour de ListView. Je dois supprimer et ajouter le POJO modifié à partir de ObservableList pour déclencher la mise à jour de ListView. Existe-t-il une possibilité d’afficher les modifications de POJOS sans la solution de contournement décrite ci-dessus?

28
user1828169

Votre question comporte plusieurs aspects (et je ne suis pas tout à fait le problème). contractez via l'activation de propriétéChangez les événements selon vos besoins ou un autre moyen - sinon, vous auriez quand même besoin d'un appui manuel du changement.

L'approche de base pour faire en sorte qu'un FX-ObservableList avertisse ses propres écouteurs des mutations d'éléments contenus consiste à le configurer avec un rappel personnalisé qui fournit un tableau d'observables. Si les éléments ont des propriétés fx, vous feriez quelque chose comme:

Callback<Person, Observable[]> extractor = new Callback<Person, Observable[]>() {

    @Override
    public Observable[] call(Person p) {
        return new Observable[] {p.lastNameProperty(), p.firstNameProperty()};
    }
};
ObservableList<Person> teamMembers = FXCollections.observableArrayList(extractor);
// fill list

Si le pojo est un noyau JavaBean à part entière, ses propriétés doivent être adaptées aux propriétés fx, f.i. en utilisant JavaBeanProperty:

Callback<PersonBean, Observable[]> extractor = new Callback<PersonBean, Observable[]>() {
    List<Property> properties = new ArrayList<Property>();
    @Override
    public Observable[] call(PersonBean arg0) {
        JavaBeanObjectProperty lastName = null;
        JavaBeanObjectProperty age = null;
        try {
            lastName = JavaBeanObjectPropertyBuilder.create()
                    .bean(arg0).name("lastName").build();
            age = JavaBeanObjectPropertyBuilder.create()
                    .bean(arg0).name("age").build();
            // hack around loosing weak references ... 
            properties.add(age);
            properties.add(lastName);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return new Observable[] {lastName, age};
    }

};
ObservableList<Person> teamMembers = FXCollections.observableArrayList(extractor);
// fill list

Notez une mise en garde: sans garder une référence forte aux propriétés adaptées quelque part, elles seront rapidement ramassées et ensuite ne sembleront plus avoir aucun effet l'éviter).

Pour tout autre moyen de notification (éventuellement grossière), vous pouvez implémenter un adaptateur personnalisé: l'adaptateur ci-dessous écoute tous les changements de propriété d'un bean. L'écoute d'autres types d'événements serait assez analogue.

/**
 * Adapt a Pojo to an Observable.
 * Note: extending ObservableValue is too much, but there is no ObservableBase ...
 *
 * @author Jeanette Winzenburg, Berlin
 */
public class PojoAdapter<T> extends ObservableValueBase<T> {

    private T bean;
    private PropertyChangeListener pojoListener;
    public PojoAdapter(T pojo) {
        this.bean = pojo;
        installPojoListener(pojo);
    }

    /**
     * Reflectively install a propertyChangeListener for the pojo, if available.
     * Silently does nothing if it cant.
     * @param item
     */
    private void installPojoListener(T item) {
        try {
            Method method = item.getClass().getMethod("addPropertyChangeListener", 
                  PropertyChangeListener.class);
            method.invoke(item, getPojoListener());
        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | 
                  IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
    /**
     * Returns the propertyChangeListener to install on each item.
     * Implemented to call notifyList.
     * 
     * @return
     */
    private PropertyChangeListener getPojoListener() {
        if (pojoListener == null) {
            pojoListener = new PropertyChangeListener() {

                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    fireValueChangedEvent();
                }
            };
        }
        return pojoListener;
    }

    @Override
    public T getValue() {
        return bean;
    }

}

Son utilisation est la même que ci-dessus (devenir ennuyeux, n'est-ce pas :-)

Callback<PersonBean, Observable[]> extractor = new Callback<PersonBean, Observable[]>() {

    @Override
    public Observable[] call(PersonBean arg0) {
        return new Observable[] {new PojoAdapter<PersonBean>(arg0)};
    }

};
ObservableList<Person> teamMembers = FXCollections.observableArrayList(extractor);
// fill list

Malheureusement, les mises à jour automatiques d'un ListView avec une telle liste géniale ne fonctionneront pas de manière fiable en raison d'un bogue qui n'a été corrigé que dans jdk8 . Dans les versions précédentes, vous revenez à la case 1 - écoutez le changement, puis mettez à jour manuellement la liste:

protected void notifyList(Object changedItem) {
    int index = list.indexOf(changedItem);
    if (index >= 0) {
        // hack around RT-28397
        //https://javafx-jira.kenai.com/browse/RT-28397
        list.set(index, null);
        // good enough since jdk7u40 and jdk8
        list.set(index, changedItem);
    }
}
17
kleopatra

Vous pouvez déclencher manuellement un ListView.EditEvent - ce qui entraînera la mise à jour de la ListView - en appelant la méthode ListView::fireEvent héritée de javafx.scene.Node. Par exemple,

/**
 * Informs the ListView that one of its items has been modified.
 *
 * @param listView The ListView to trigger.
 * @param newValue The new value of the list item that changed.
 * @param i The index of the list item that changed.
 */
public static <T> void triggerUpdate(ListView<T> listView, T newValue, int i) {
    EventType<? extends ListView.EditEvent<T>> type = ListView.editCommitEvent();
    Event event = new ListView.EditEvent<>(listView, type, newValue, i);
    listView.fireEvent(event);
}

Ou comme une ligne,

listView.fireEvent(new ListView.EditEvent<>(listView, ListView.editCommitEvent(), newValue, i));

Voici un exemple d'application pour démontrer son utilisation.

/**
 * An example of triggering a JavaFX ListView when an item is modified.
 * 
 * Displays a list of strings.  It iterates through the strings adding
 * exclamation marks with 2 second pauses in between.  Each modification is
 * accompanied by firing an event to indicate to the ListView that the value
 * has been modified.
 * 
 * @author Mark Fashing
 */
public class ListViewTest extends Application {

    /**
     * Informs the ListView that one of its items has been modified.
     *
     * @param listView The ListView to trigger.
     * @param newValue The new value of the list item that changed.
     * @param i The index of the list item that changed.
     */    
    public static <T> void triggerUpdate(ListView<T> listView, T newValue, int i) {
        EventType<? extends ListView.EditEvent<T>> type = ListView.editCommitEvent();
        Event event = new ListView.EditEvent<>(listView, type, newValue, i);
        listView.fireEvent(event);
    }

    @Override
    public void start(Stage primaryStage) {
        // Create a list of mutable data.  StringBuffer works nicely.
        final List<StringBuffer> listData = Stream.of("Fee", "Fi", "Fo", "Fum")
                .map(StringBuffer::new)
                .collect(Collectors.toList());
        final ListView<StringBuffer> listView = new ListView<>();
        listView.getItems().addAll(listData);
        final StackPane root = new StackPane();
        root.getChildren().add(listView);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
        // Modify an item in the list every 2 seconds.
        new Thread(() -> {
            IntStream.range(0, listData.size()).forEach(i -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(listData.get(i));
                Platform.runLater(() -> {
                    // Where the magic happens.
                    listData.get(i).append("!");
                    triggerUpdate(listView, listData.get(i), i);
                });            
            });
        }).start();
    }

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

}
5
Mark

En utilisant l’idée de Francis, j’ai fait:

   list.set(list.indexOf(POJO), POJO);

Peut-être pas la meilleure solution mais a fonctionné.

3
emorgado

Depuis que Java 8u60, ListView prend officiellement en charge une méthode refresh() pour mettre à jour la vue manuellement. JavaDoc:

Ceci est utile dans les cas où la source de données sous-jacente a été modifiée d'une manière qui n'est pas observée par le ListView lui-même.

J'ai utilisé avec succès cette méthode pour ce problème ici pour mettre à jour le contenu des éléments dans ListView.

2
pathfinder78

Vous devez prendre la liste observable et mettre à jour l'objet à l'aide de list.set (selectedIndex, object); Mon exemple montrant le bouton avec la méthode de la poignée. En cela, j'ai édité la liste des utilisateurs dans fx viewtable

Button commit = new Button("Commit");
    commit.setOnAction(new EventHandler<ActionEvent>() {
        public void handle(ActionEvent evt) {
            int selectedIndex = tableView.getSelectionModel().getSelectedIndex();
            User user = tableView.getSelectionModel().getSelectedItem();
            user.setId(Integer.parseInt(idTF.getText()));
            user.setName(nameCB.getValue());
            user.setSurname(srnameTF.getText());
            user.setAddress(addressTF.getText());
            service.getUsers().set(selectedIndex, user);
            tableView.toFront();
        }
    });
1
milosz