Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
246 views
in Technique[技术] by (71.8m points)

java - Best practice to decorate an ObservableList and retain change events

My datasource provides an ObservableList<String>, however for my ListView I need an ObservableList<Warning>.

A Warning is basically just a decorator for the strings, adding a boolean to provide a means for tracking the state of checkboxes my ListView, as proposed in this answer.

class Warning {

    private final ReadOnlyStringWrapper name;
    private final BooleanProperty checked;

    /* ... */

}

Currently I am observing change-events in the original list and add/remove items in the warnings list manually:

ObservableList<String> stringList = ...;
ObservableList<Warning> warningList = ...;

stringList.addListener(new ListChangeListener<String>() {

    @Override
    public void onChanged(ListChangeListener.Change<? extends String> change) {
        if (change.wasAdded()) {
            warningList.addAll(change.getAddedSubList().stream().map(Warning::new).collect(Collectors.toList()));
        } else if (change.wasRemoved()) {
            change.getRemoved().forEach(str -> {
                warningList.removeIf(w -> str.equals(w.name));
            });
        }
    }

});

My question is: Is there a more elegant way to decorate my String-typed list, so it can be used as a Warning-typed list without manually passing through change events?

To be more precise: If a string is added to or removed from the original list, I want to see this change immediately in the Warnings-list and thus the ListView.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I've been thinking about this a bit since you posted it. Using EasyBind as I had suggested in the comment won't work, since it would create a new Warning every time you called get(...) on the mapped list. So

stringList.add("foo");
warningList.get(0).setChecked(true);
assert warningList.get(0).isChecked();

would fail.

Additionally, your mechanism goes wrong (I think) if you have duplicate entries in the source list (stringList), as you would remove all corresponding entries from the warningList when a single element was removed from the stringList. In fact, getting the removed elements correct is quite tricky.

Here is a solution based on Tomas Mikula's MappedList which caches the mapping between the source elements and the mapped elements. It uses an IdentityHashMap to ensure that duplicate elements behave correctly in both lists. Note this only works for the specific case where you want to create new objects when items are added to the source list, so it is not intended (and would not work) as a replacement for the mechanism in EasyBind.

import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.transformation.TransformationList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class WrappedObjectListExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        ObservableList<String> stringList = FXCollections.observableArrayList("One", "Two", "Three");
        ObservableList<Warning> warningList = new CachingMappedList<Warning, String>(stringList, Warning::new);

        ListView<String> stringListView = new ListView<>(stringList);
        ListView<Warning> warningListView = new ListView<>(warningList);

        warningListView.setCellFactory(CheckBoxListCell.forListView(Warning::checkedProperty));

        TextField textField = new TextField();
        textField.setOnAction(e -> {
            if (! textField.getText().isEmpty()) {
                stringList.add(textField.getText());
                textField.setText("");
            }
        });

        Button remove = new Button("Remove");
        remove.setOnAction(e -> stringList.remove(stringListView.getSelectionModel().getSelectedIndex()));
        remove.disableProperty().bind(stringListView.getSelectionModel().selectedItemProperty().isNull());

        HBox lists = new HBox(10, stringListView, warningListView);
        VBox root = new VBox(10, lists, textField, remove);
        root.setPadding(new Insets(20));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

    public static class Warning {
        private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
        private final BooleanProperty checked = new SimpleBooleanProperty();

        public Warning(String name) {
            this.name.set(name);
        }

        public final ReadOnlyStringProperty nameProperty() {
            return this.name.getReadOnlyProperty();
        }

        public final String getName() {
            return this.nameProperty().get();
        }

        public final BooleanProperty checkedProperty() {
            return this.checked;
        }

        public final boolean isChecked() {
            return this.checkedProperty().get();
        }

        public final void setChecked(final boolean checked) {
            this.checkedProperty().set(checked);
        }

        @Override
        public String toString() {
            return getName();
        }

    }

    public static class CachingMappedList<S,T> extends TransformationList<S, T> {

        private final Function<T, S> mapper ;

        private final IdentityHashMap<T, S> cache ;

        public CachingMappedList(ObservableList<T> source, Function<T,S> mapper) {
            super(source);
            this.mapper = mapper ;
            this.cache = new IdentityHashMap<>();
        }

        @Override
        protected void sourceChanged(Change<? extends T> c) {

            fireChange(new Change<S>(this) {

                @Override
                public boolean wasAdded() {
                    return c.wasAdded();
                }

                @Override
                public boolean wasRemoved() {
                    return c.wasRemoved();
                }

                @Override
                public boolean wasReplaced() {
                    return c.wasReplaced();
                }

                @Override
                public boolean wasUpdated() {
                    return c.wasUpdated();
                }

                @Override
                public boolean wasPermutated() {
                    return c.wasPermutated();
                }


                @Override
                public boolean next() {
                    return c.next();
                }

                @Override
                public void reset() {
                    c.reset();
                }

                @Override
                public int getFrom() {
                    return c.getFrom();
                }

                @Override
                public int getTo() {
                    return c.getTo();
                }

                @Override
                public List<S> getRemoved() {
                    List<S> removed = new ArrayList<>();
                    c.getRemoved().forEach(t -> removed.add(cache.get(t)));
                    return removed;
                }

                @Override
                public int getPermutation(int i) {
                    return c.getPermutation(i);
                }

                @Override
                protected int[] getPermutation() {
                    throw new AssertionError("Unreachable code");
                }

            });

            // clean up cache:

            c.reset();
            while (c.next()) {
                if (c.wasRemoved()) {
                    c.getRemoved().forEach(cache::remove);
                }
            }            
        }

        @Override
        public int getSourceIndex(int index) {
            return index ;
        }

        @Override
        public S get(int index) {
            return cache.computeIfAbsent(getSource().get(index), mapper);
        }

        @Override
        public int size() {
            return getSource().size();
        }
    }

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...