javafxmirrorobservablelist

mirror one observableList to another


  1. In JavaFX, I have an ObservableList of objects, and want another ObservableList that will mirror the first list but contain a String representation of each object. Is there anything simpler than writing a custom ListChangeListener to do the conversion ? I have a StringConverter which can provide the mirrored value.

  2. Similarly, given an ObservableList<String>, how do I create a second ObservableList<String> that has a constant entry at index 0, and mirrors the first list beginning at index 1?


Solution

  • For the first question, the easiest way to do this is to use the EasyBind framework. Then it is as simple as

    ObservableList<String> stringList = EasyBind.map(myBaseList, myConverter::toString);
    

    Here is an SSCCE using EasyBind:

    import org.fxmisc.easybind.EasyBind;
    
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ListChangeListener.Change;
    import javafx.collections.ObservableList;
    import javafx.util.StringConverter;
    
    
    public class MappedAndTransformedListExample {
    
        public static void main(String[] ags) {
            ObservableList<Person> baseList = FXCollections.observableArrayList(
                    new Person("Jacob", "Smith", "jacob.smith@example.com"),
                    new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                    new Person("Ethan", "Williams", "ethan.williams@example.com"),
                    new Person("Emma", "Jones", "emma.jones@example.com")
            );
    
            StringConverter<Person> converter = new StringConverter<Person>() {
    
                @Override
                public String toString(Person person) {
                    return person.getFirstName() + " " + person.getLastName();
                }
    
                @Override
                public Person fromString(String string) {
                    int indexOfDelimiter = string.indexOf(' ');
                    return new Person(string.substring(0, indexOfDelimiter), 
                            string.substring(indexOfDelimiter+1), 
                            "");
                }
    
            };
    
            ObservableList<String> namesList = EasyBind.map(baseList, converter::toString);
    
            namesList.forEach(System.out::println);
    
            namesList.addListener((Change<? extends String> c) -> {
                while (c.next()) {
                    if (c.wasAdded()) {
                        System.out.println("Added "+c.getAddedSubList());
                    }
                }
            });
    
            System.out.println("\nAdding Michael to base list...\n");
            baseList.add(new Person("Michael", "Brown", "michael.brown@example.com"));
    
            namesList.forEach(System.out::println);
        }
    
    
    
        public static class Person {
            private final StringProperty firstName = new SimpleStringProperty(this, "firstName");
            private final StringProperty lastName = new SimpleStringProperty(this, "lastName");
            private final StringProperty email = new SimpleStringProperty(this, "email");
    
            public Person(String firstName, String lastName, String email) {
                this.firstName.set(firstName);
                this.lastName.set(lastName);
                this.email.set(email);
            }
    
            public final StringProperty firstNameProperty() {
                return this.firstName;
            }
    
            public final String getFirstName() {
                return this.firstNameProperty().get();
            }
    
            public final void setFirstName(final String firstName) {
                this.firstNameProperty().set(firstName);
            }
    
            public final StringProperty lastNameProperty() {
                return this.lastName;
            }
    
            public final java.lang.String getLastName() {
                return this.lastNameProperty().get();
            }
    
            public final void setLastName(final java.lang.String lastName) {
                this.lastNameProperty().set(lastName);
            }
    
            public final StringProperty emailProperty() {
                return this.email;
            }
    
            public final java.lang.String getEmail() {
                return this.emailProperty().get();
            }
    
            public final void setEmail(final java.lang.String email) {
                this.emailProperty().set(email);
            }
    
        }
    }
    

    If you prefer not to use a third-party framework for some reason, you can use a TransformationList (which is what EasyBind does under the hood: I copied the code below from the source code there and modified it).

    In the above, you would replace

    ObservableList<String> namesList = EasyBind.map(baseList, converter::toString);
    

    with

        ObservableList<String> namesList = new TransformationList<String, Person>(baseList) {
    
            @Override
            public int getSourceIndex(int index) {
                return index ;
            }
    
            @Override
            public String get(int index) {
                return converter.toString(getSource().get(index));
            }
    
            @Override
            public int size() {
                return getSource().size();
            }
    
            @Override
            protected void sourceChanged(Change<? extends Person> c) {
                fireChange(new Change<String>(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 int getPermutation(int i) {
                        return c.getPermutation(i);
                    }
    
                    @Override
                    protected int[] getPermutation() {
                        // This method is only called by the superclass methods
                        // wasPermutated() and getPermutation(int), which are
                        // both overriden by this class. There is no other way
                        // this method can be called.
                        throw new AssertionError("Unreachable code");
                    }
    
                    @Override
                    public List<String> getRemoved() {
                        ArrayList<String> res = new ArrayList<>(c.getRemovedSize());
                        for(Person removedPerson: c.getRemoved()) {
                            res.add(converter.toString(removedPerson));
                        }
                        return res;
                    }
    
                    @Override
                    public int getFrom() {
                        return c.getFrom();
                    }
    
                    @Override
                    public int getTo() {
                        return c.getTo();
                    }
    
                    @Override
                    public boolean next() {
                        return c.next();
                    }
    
                    @Override
                    public void reset() {
                        c.reset();
                    }
                });
            }
    
    
        };
    

    For the second question, you must use a transformation list. Here's an updated main(...) method that shows how to do this. (It works just as well with the second version of part 1.)

    public static void main(String[] ags) {
        ObservableList<Person> baseList = FXCollections.observableArrayList(
                new Person("Jacob", "Smith", "jacob.smith@example.com"),
                new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person("Ethan", "Williams", "ethan.williams@example.com"),
                new Person("Emma", "Jones", "emma.jones@example.com")
        );
    
        StringConverter<Person> converter = new StringConverter<Person>() {
    
            @Override
            public String toString(Person person) {
                return person.getFirstName() + " " + person.getLastName();
            }
    
            @Override
            public Person fromString(String string) {
                int indexOfDelimiter = string.indexOf(' ');
                return new Person(string.substring(0, indexOfDelimiter), 
                        string.substring(indexOfDelimiter+1), 
                        "");
            }
    
        };
    
        ObservableList<String> namesList = EasyBind.map(baseList, converter::toString);
    
        ObservableList<String> namesListWithHeader = new TransformationList<String, String>(namesList) {
    
            @Override
            public int getSourceIndex(int index) {
                return index - 1 ;
            }
    
            @Override
            public String get(int index) {
                if (index == 0) {
                    return "Contacts";
                } else {
                    return getSource().get(index - 1);
                }
            }
    
            @Override
            public int size() {
                return getSource().size() + 1 ;
            }
    
            @Override
            protected void sourceChanged(Change<? extends String> c) {
                fireChange(new Change<String>(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 int getPermutation(int i) {
                        return c.getPermutation(i - 1) + 1;
                    }
    
                    @Override
                    protected int[] getPermutation() {
                        // This method is only called by the superclass methods
                        // wasPermutated() and getPermutation(int), which are
                        // both overriden by this class. There is no other way
                        // this method can be called.
                        throw new AssertionError("Unreachable code");
                    }
    
                    @Override
                    public List<String> getRemoved() {
                        ArrayList<String> res = new ArrayList<>(c.getRemovedSize());
                        for(String removed: c.getRemoved()) {
                            res.add(removed);
                        }
                        return res;
                    }
    
                    @Override
                    public int getFrom() {
                        return c.getFrom() + 1;
                    }
    
                    @Override
                    public int getTo() {
                        return c.getTo() + 1;
                    }
    
                    @Override
                    public boolean next() {
                        return c.next();
                    }
    
                    @Override
                    public void reset() {
                        c.reset();
                    }
                });
            }
    
        };
    
        namesListWithHeader.forEach(System.out::println);
    
        namesListWithHeader.addListener((Change<? extends String> c) -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    System.out.println("Added "+c.getAddedSubList());
                    System.out.println("From: "+c.getFrom()+", To: "+c.getTo());
                }
            }
        });
    
        System.out.println("\nAdding Michael to base list...\n");
        baseList.add(new Person("Michael", "Brown", "michael.brown@example.com"));
    
        namesListWithHeader.forEach(System.out::println);
    }