简体   繁体   中英

Binding to a ObservableValue<ObservableList> instead of an ObservableList with EasyBind

I've got a master/detail panel with ModelItem items. Each ModelItem has a ListProperty<ModelItemDetail> , and each ModelItemDetail has a few StringProperty s.

In the detail panel, I want to show a Label that will have its text bounded to and derived from the properties of each ModelItemDetail of the currently selected ModelItem . The final value may depend on other external properties, such as having a CheckBox on the Detail panel selected (ie if the checkbox is selected, the values of bProperty aren't included in the result).

This binding accomplishes what I want, using Bindings.createStringBinding() :

ObservableValue<ModelItem> selectedItemBinding = EasyBind.monadic(mdModel.selectedItemProperty()).orElse(new ModelItem());

// API Label Binding
apiLabel.textProperty().bind(Bindings.createStringBinding( 
    () -> selectedItemBinding.getValue().getDetails().stream()
        .map(i -> derivedBinding(i.aProperty(), i.bProperty()))
        .map(v->v.getValue())
        .collect(Collectors.joining(", "))
    , mdModel.selectedItemProperty(), checkBox.selectedProperty()));

With for instance:

private ObservableValue<String> derivedBinding(ObservableValue<String> aProp, ObservableValue<String> bProp) {
    return EasyBind.combine(aProp, bProp, checkBox.selectedProperty(), 
            (a, b, s) -> Boolean.TRUE.equals(s) ? new String(a + " <" + b + ">") : a);
}

I recently found out about EasyBind, and I'm trying to replace some API Bindings with it. I can't find a way to express this binding with EasyBind. Apparently, the main problem with my code is that because the selectedItem is a property, I can't use its details as an ObservableList , and I must stick to an ObservableValue<ObservableList>> . This is inconvenient to chain the transformations through EasyBind.map(ObservableList) and EasyBind.combine(ObservableList) , which seem ideal candidates to implement this binding. At some point I've thought of creating a local ListProperty and binding it to the selectedItem's details through a listener on selectedItem, but it just looks too verbose and unclean.

I've tried forcing the EasyBind API like this:

ObservableValue<ObservableList<ModelItemDetail>> ebDetailList = EasyBind.select(selectedItemBinding).selectObject(ModelItem::detailsProperty);
MonadicObservableValue<ObservableList<ObservableValue<String>>> ebDerivedList = EasyBind.monadic(ebDetailList).map(x->EasyBind.map(x, i -> derivedBinding(i.aProperty(), i.bProperty())));
MonadicObservableValue<ObservableValue<String>> ebDerivedValueBinding = ebDerivedList.map(x->EasyBind.combine(x, stream -> stream.collect(Collectors.joining(", "))));
easyBindLabel.textProperty().bind(ebDerivedValueBinding.getOrElse(new ReadOnlyStringWrapper("Nothing to see here, move on")));

But I've got the feeling the last getOrElse is only getting invoked at initialization time and doesn't update when selectedItem changes.

I've also tried getting the ObservableList right away, but couldn't expect any other thing that an empty list:

ObservableList<ModelItemDetail> ebDetailList = EasyBind.select(selectedItemBinding).selectObject(ModelItem::detailsProperty).get();
ObservableList<ObservableValue<String>> ebDerivedList = EasyBind.map(ebDetailList, i -> derivedBinding(i.aProperty(), i.bProperty()));
ObservableValue<String> ebDerivedValueBinding = EasyBind.combine(ebDerivedList, stream -> stream.collect(Collectors.joining(", "))).orElse("Nothing to see here, move on");
easyBindLabel2.textProperty().bind(ebDerivedValueBinding);

I've even tried using EasyBind.subscribe to listen to selectedItem changes and re-bind (not too sure about this but I don't think rebinding would be needed, everything is there to perform the calculations):

EasyBind.subscribe(selectedItemBinding, newValue -> {
    if (newValue != null) {
            ObservableList<ObservableValue<String>> l = 
                EasyBind.map(newValue.getDetails(), 
                             i -> derivedBinding(i.aProperty(), i.bProperty()));
            easyBindLabelSub.textProperty().bind(
                    EasyBind.combine(l, 
                            strm -> strm.collect(Collectors.joining(", "))
                    ));}});

This works partially, actually it's listening to checkbox changes but curiously only to the first change. I don't have a clue why (would be great knowing). If I add another EasyBind.Subscribe to subscribe to checkbox.selectedProperty, it works as intended, but this also too verbose and unclean. Same happens if I add an API listener myself to the selectedItemProperty and perform the binding there.

My motivation to use EasyBind to express this binding is precisely get rid of the need to explicitly express dependencies for the binding, and try to simplify it further. All the approaches i've come up with are notably worse than the API one, as much as I'm not fully satisfied with it.

I'm still quite new to JavaFX and I'm trying to wrap my head around this. I want to understand what's happening, and find out if there is a short concise and elegant way to express this Binding with EasyBind. I'm starting to wonder if EasyBind just isn't prepared for this use case (which by the way I don't think is that rare). Probably I am missing something trivial, though.

Here is a MVCE showing a few of the approaches I've tried, and the API Binding working as intended:

package mcve.javafx;

import java.util.*;
import java.util.stream.*;

import javafx.application.*;
import javafx.beans.binding.*;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.*;

import org.fxmisc.easybind.*;
import org.fxmisc.easybind.monadic.*;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    private CheckBox checkShowMore;

    @Override
    public void start(Stage primaryStage) {
        try {
            // Initialize model
            MasterDetailModel mdModel = new MasterDetailModel();
            ObservableList<ModelItem> itemsList = FXCollections.observableArrayList();
            for (int i=0;i<5;i++) { itemsList.add(newModelItem(i)); }

            // Master
            ListView<ModelItem> listView = new ListView<ModelItem>();
            listView.setItems(itemsList);
            listView.setPrefHeight(150);
            mdModel.selectedItemProperty().bind(listView.getSelectionModel().selectedItemProperty());

            //Detail
            checkShowMore = new CheckBox();
            checkShowMore.setText("Show more details");
            VBox detailVBox = new VBox();           
            Label apiLabel = new Label();
            Label easyBindLabel = new Label();
            Label easyBindLabel2 = new Label();
            Label easyBindLabelSub = new Label();
            Label easyBindLabelLis = new Label();
            detailVBox.getChildren().addAll(
                    checkShowMore, 
                    new TitledPane("API Binding", apiLabel), 
                    new TitledPane("EasyBind Binding", easyBindLabel),
                    new TitledPane("EasyBind Binding 2", easyBindLabel2),
                    new TitledPane("EasyBind Subscribe", easyBindLabelSub),
                    new TitledPane("Listener+EasyBind Approach", easyBindLabelLis)
            );

            // Scene
            Scene scene = new Scene(new VBox(listView, detailVBox),400,400);
            primaryStage.setScene(scene);
            primaryStage.setTitle("JavaFX/EasyBind MVCE");

            // --------------------------
            // -------- BINDINGS --------
            // --------------------------
            ObservableValue<ModelItem> selectedItemBinding = EasyBind.monadic(mdModel.selectedItemProperty()).orElse(new ModelItem());

            // API Label Binding
            apiLabel.textProperty().bind(Bindings.createStringBinding( 
                () -> selectedItemBinding.getValue().getDetails().stream()
                    .map(i -> derivedBinding(i.aProperty(), i.bProperty()))
                    .map(v->v.getValue())
                    .collect(Collectors.joining(", "))
                , mdModel.selectedItemProperty(), checkShowMore.selectedProperty()));

            // EasyBind Binding Approach 1
            {
            ObservableValue<ObservableList<ModelItemDetail>> ebDetailList = EasyBind.select(selectedItemBinding).selectObject(ModelItem::detailsProperty);
            MonadicObservableValue<ObservableList<ObservableValue<String>>> ebDerivedList = EasyBind.monadic(ebDetailList).map(x->EasyBind.map(x, i -> derivedBinding(i.aProperty(), i.bProperty())));
            MonadicObservableValue<ObservableValue<String>> ebDerivedValueBinding = ebDerivedList.map(x->EasyBind.combine(x, stream -> stream.collect(Collectors.joining(", "))));
            easyBindLabel.textProperty().bind(ebDerivedValueBinding.getOrElse(new ReadOnlyStringWrapper("Nothing to see here, move on")));
            }

            // EasyBind Binding Approach 2
            {
            ObservableList<ModelItemDetail> ebDetailList = EasyBind.select(selectedItemBinding).selectObject(ModelItem::detailsProperty).get();
            ObservableList<ObservableValue<String>> ebDerivedList = EasyBind.map(ebDetailList, i -> derivedBinding(i.aProperty(), i.bProperty()));
            ObservableValue<String> ebDerivedValueBinding = EasyBind.combine(ebDerivedList, stream -> stream.collect(Collectors.joining(", "))).orElse("Nothing to see here, move on");
            easyBindLabel2.textProperty().bind(ebDerivedValueBinding);
            }

            // Subscribe approach
            EasyBind.subscribe(selectedItemBinding, newValue -> {
                if (newValue != null) {
                        ObservableList<ObservableValue<String>> l = EasyBind.map(newValue.getDetails(), i -> derivedBinding(i.aProperty(), i.bProperty()));
                        easyBindLabelSub.textProperty().bind(
                                EasyBind.combine(l, 
                                        strm -> strm.collect(Collectors.joining(", "))
                                ));
                }
            });
            //With this it works as intended, but something feels very wrong about this
             /*
            EasyBind.subscribe(checkShowMore.selectedProperty(), newValue -> {
                if (selectedItemBinding != null) {
                        ObservableList<ObservableValue<String>> l = EasyBind.map(selectedItemBinding.getValue().getDetails(), i -> derivedBinding(i.aProperty(), i.bProperty()));
                        easyBindLabelSub.textProperty().bind(
                                EasyBind.combine(l, 
                                        strm -> strm.collect(Collectors.joining(", "))
                                ));
                }
                });
            */

            // Listener approach
            selectedItemBinding.addListener( (ob, o, n) -> {
                ObservableList<ModelItemDetail> ebDetailList = n.getDetails();
                ObservableList<ObservableValue<String>> ebDerivedList = EasyBind.map(ebDetailList, i -> derivedBinding(i.aProperty(), i.bProperty()));
                ObservableValue<String> ebDerivedValueBinding = EasyBind.combine(ebDerivedList, stream -> stream.collect(Collectors.joining(", "))).orElse("Nothing to see here, move on");
                easyBindLabelLis.textProperty().bind(ebDerivedValueBinding);                
            });





            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    private ObservableValue<String> derivedBinding(ObservableValue<String> aProp, ObservableValue<String> bProp) {
        return EasyBind.combine(aProp, bProp, checkShowMore.selectedProperty(), 
                (a, b, s) -> Boolean.TRUE.equals(s) ? new String(a + " <" + b + ">") : a);
    }   

    private ModelItem newModelItem(int number) { 
        ModelItem item = new ModelItem();
        item.itemNumber = number+1;
        for (int i=0;i<2;i++) { 
            ModelItemDetail detail = new ModelItemDetail();
            detail.setA("A" + (i+item.itemNumber));
            detail.setB("B" + (i+item.itemNumber));
            item.getDetails().add(detail);
        }
        return item;
    }

    /** GUI Model class */ 
    private static class MasterDetailModel {
        private ObjectProperty<ModelItem> selectedItemProperty = new SimpleObjectProperty<>();
        public ObjectProperty<ModelItem> selectedItemProperty() { return selectedItemProperty; }
        public ModelItem getSelectedItem() { return selectedItemProperty.getValue(); }
        public void setSelectedItem(ModelItem item) { selectedItemProperty.setValue(item); }
    }

    /** Domain Model class */
    private static class ModelItem { 
        int itemNumber;
        private ListProperty<ModelItemDetail> detailsProperty = new SimpleListProperty<>(FXCollections.observableArrayList());
        public ListProperty<ModelItemDetail> detailsProperty() { return detailsProperty; }
        public ObservableList<ModelItemDetail> getDetails() { return detailsProperty.getValue(); }
        public void setDetails(List<ModelItemDetail> details) { detailsProperty.setValue(FXCollections.observableList(details)); }
        public String toString() { return "Item " + itemNumber; }
    }

    /** Domain Model class */
    private static class ModelItemDetail {
        private StringProperty aProperty = new SimpleStringProperty();
        public StringProperty aProperty() { return aProperty; }
        public String getA() { return aProperty.get(); }
        public void setA(String a) { aProperty.set(a); }

        private StringProperty bProperty = new SimpleStringProperty();
        public StringProperty bProperty() { return bProperty; }
        public String getB() { return bProperty.get(); }
        public void setB(String b) { bProperty.set(b); }
    }
}

UPDATE : I've made some progress.

The following code works fine, but mysteriouysly still keeps listening only to the first change on the CheckBox:

ListProperty<ModelItemDetail> obsList = new SimpleListProperty<>(FXCollections.observableArrayList(i->new Observable[] { i.aProperty(), i.bProperty(), checkShowMore.selectedProperty()}));
obsList.bind(selectedItemBinding.flatMap(ModelItem::detailsProperty));
ObservableList<ModelItemDetail> ebDetailList = obsList; // WHY ??
ObservableList<ObservableValue<String>> ebDerivedList = EasyBind.map(ebDetailList, i -> derivedBinding(i.aProperty(), i.bProperty()));
ObservableValue<String> ebDerivedValueBinding = EasyBind.combine(ebDerivedList, stream -> stream.collect(Collectors.joining(", "))).orElse("Nothing to see here, move on");
labelPlayground.textProperty().bind(ebDerivedValueBinding);

Apparently the main reason I was having trouble is because I didn't see how to get an ObservableList from the bound current selectedItem with the EasyBind fluent API. Declaring a local ListProperty and binding it to the selected item's I could take advantage of ListProperty being an ObservableList . I think EasyBind somewhere doesn't follow. Feels like type information is getting lost somewhere. I can't put together all these variables in this last code, and I don't understand why EasyBind.map() will accept ebDetailList in this last code, but won't accept obsList .

So, the question now is, why is this binding listening to CheckBox events only the first time? The extractor in the backing list for the ListProperty doesn't do anything. I guess the obsList.bind() is replacing the backing list with the one in the Model, which has no extractors.

If I understand correctly, you want the label to display text for a selected ModelItem , that text is composed of all the ModelItemDetail it contains. This text should update whenever a ModelItemDetail is added or removed, and when the a or b properties of any of the ModelItemDetail s in its list are updated.

You don't need an external library for this 1-level depth binding ( ModelItemDetail -> a , b ). Changes to the list of ModelItemDetail s are reported by the ObservableList . Changes to the properties of items within the list can be reported by an extractor :

ListProperty<ModelItemDetail> detailsProperty = new SimpleListProperty<>(
                FXCollections.observableArrayList(i -> new Observable[]{i.aProperty(), i.bProperty()}));

In fact, you don't need a ListProperty for this, a simple ObservableList will suffice.

In the below example,

  • A single ModelItem is shown in The ListView . It is initialized with 3 ModelItemDetail with some a and b properties.
  • The text label on the bottom shows the text of the combined ModelItemDetail s.
  • The CheckBox on the top determines whether the b property will be displayed. Note that even when it is not selected, changes to b will continue to be reported (but not shown).
  • The "Add item detail" button to the right will add another randomly numbered ModelItemDetail to the list. This change will reflect immediately through the ObservableList .
  • The "Change some A" button to the right will set the value of the a property of a randomly selected ModelItemDetail from the list. This change will be reflected immediately through the ObservableList 's extractor.

public class Main extends Application {

    public Main() {}

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

    @Override
    public void start(Stage stage) throws Exception {
        // Mock initial data
        ModelItem item = new ModelItem();
        ModelItemDetail mid1 = new ModelItemDetail();
        mid1.setA("a1");
        mid1.setB("b1");
        ModelItemDetail mid2 = new ModelItemDetail();
        mid2.setA("a2");
        mid2.setB("b2");
        ModelItemDetail mid3 = new ModelItemDetail();
        mid3.setA("a3");
        mid3.setB("b3");
        ObservableList<ModelItemDetail> details = item.getDetails();
        details.add(mid1);
        details.add(mid2);
        details.add(mid3);

        // Create binding

        CheckBox showB = new CheckBox("Show b");
        Label label = new Label();

        label.textProperty().bind(Bindings.createStringBinding(() -> {
            return details.stream()
                .map(mid ->
                    Boolean.TRUE.equals(showB.isSelected()) ? new String(mid.getA() + " <" + mid.getB() + ">") : mid.getA()
                ).collect(Collectors.joining(", "));
        }, details, showB.selectedProperty()));

        // Create testing components

        Button add = new Button("Add item detail");
        add.setOnAction(e -> {
            Random r = new Random();
            int i = r.nextInt(100) + 3;
            ModelItemDetail mid = new ModelItemDetail();
            mid.setA("a" + i);
            mid.setB("b" + i);
            details.add(mid);
        });
        Button changeA = new Button("Change some A");
        changeA.setOnAction(e -> {
            Random r = new Random();
            ModelItemDetail detail = details.get(r.nextInt(details.size()));
            detail.setA("a" + r.nextInt(100) + 3);
        });

        // Display everything

        BorderPane pane = new BorderPane();
        ListView<ModelItem> list = new ListView<>();
        list.getItems().add(item);
        pane.setCenter(list);
        pane.setRight(new VBox(add, changeA));
        pane.setTop(showB);
        pane.setBottom(label);
        stage.setScene(new Scene(pane));
        stage.show();
    }

    private static class ModelItem {
        int itemNumber;
        private ObservableList<ModelItemDetail> detailsProperty = FXCollections.observableArrayList(i -> new Observable[]{i.aProperty(), i.bProperty()});
        public ObservableList<ModelItemDetail> getDetails() { return detailsProperty; }
        @Override public String toString() { return "Item " + itemNumber; }
    }

    /** Domain Model class */
    private static class ModelItemDetail {
        private StringProperty aProperty = new SimpleStringProperty();
        public StringProperty aProperty() { return aProperty; }
        public String getA() { return aProperty.get(); }
        public void setA(String a) { aProperty.set(a); }

        private StringProperty bProperty = new SimpleStringProperty();
        public StringProperty bProperty() { return bProperty; }
        public String getB() { return bProperty.get(); }
        public void setB(String b) { bProperty.set(b); }
    }
}

You can add more ModelItem s to the ListView and have the label display the text for the selected one.

After some time and practice and getting more familiar with Bindings, Properties and Observables, I came up with what I was looking for. A simple, powerful, concise and type-safe EasyBind expression that doesn't need listeners, duplicating or explicitly stating binding dependencies, or extractors. Definitely looks much better than the Bindings API version.

 labelWorking.textProperty().bind(
    selectedItemBinding
    .flatMap(ModelItem::detailsProperty)
    .map(l -> derivedBinding(l))
    .flatMap(l -> EasyBind.combine(
             l, stream -> stream.collect(Collectors.joining(", "))))
    );

With

private ObservableList<ObservableValue<String>> derivedBinding(ObservableList<ModelItemDetail> l) { 
        return l.stream()
                .map(c -> derivedBinding(c.aProperty(), c.bProperty()))
                .collect(Collectors.toCollection(FXCollections::observableArrayList));
    }

There are apparently some bugs with type inference in Eclipse/javac. That didn't help getting things clear when I was trying to find the right expression letting the IDE guide me.

The MVCE with the working binding for the sake of completeness:

package mcve.javafx;

import java.util.List;
import java.util.stream.Collectors;

import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.monadic.MonadicBinding;

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    private CheckBox checkShowMore;

    @Override
    public void start(Stage primaryStage) {
        try {


            // Initialize model
            MasterDetailModel mdModel = new MasterDetailModel();
            ObservableList<ModelItem> itemsList = FXCollections.observableArrayList();
            for (int i=0;i<5;i++) { itemsList.add(newModelItem(i)); }

            MonadicBinding<ModelItem> selectedItemBinding = EasyBind.monadic(mdModel.selectedItemProperty()).orElse(new ModelItem());

            // Master
            ListView<ModelItem> listView = new ListView<ModelItem>();
            listView.setItems(itemsList);
            listView.setPrefHeight(150);
            mdModel.selectedItemProperty().bind(listView.getSelectionModel().selectedItemProperty());

            //Detail
            checkShowMore = new CheckBox();
            checkShowMore.setText("Show more details");
            VBox detailVBox = new VBox();           
            Label apiLabel = new Label();
            Label labelPlayground = new Label();
            detailVBox.getChildren().addAll(
                    checkShowMore, 
                    new TitledPane("API Binding", apiLabel), 
                    new TitledPane("EasyBind", labelPlayground)
            );


            // Scene
            Scene scene = new Scene(new VBox(listView, detailVBox),400,400);
            primaryStage.setScene(scene);
            primaryStage.setTitle("JavaFX/EasyBind MVCE");

            // --------------------------
            // -------- BINDINGS --------
            // --------------------------

            // API Label Binding

            apiLabel.textProperty().bind(Bindings.createStringBinding( 
                () -> selectedItemBinding.getValue().getDetails().stream()
                    .map(i -> derivedBinding(i.aProperty(), i.bProperty()))
                    .map(v->v.getValue())
                    .collect(Collectors.joining(", "))
                , mdModel.selectedItemProperty(), checkShowMore.selectedProperty()));

            // EasyBind non-working attempt
            /*
            ListProperty<ModelItemDetail> obsList = new SimpleListProperty<>(FXCollections.observableArrayList(i->new Observable[] { i.aProperty(), i.bProperty(), checkShowMore.selectedProperty()}));
            obsList.bind(selectedItemBinding.flatMap(ModelItem::detailsProperty));
            ObservableList<ModelItemDetail> ebDetailList = obsList; // WHY ??
            ObservableList<ObservableValue<String>> ebDerivedList = EasyBind.map(ebDetailList, i -> derivedBinding(i.aProperty(), i.bProperty()));
            ObservableValue<String> ebDerivedValueBinding = EasyBind.combine(ebDerivedList, stream -> stream.collect(Collectors.joining(", "))).orElse("Nothing to see here, move on");
            labelPlayground.textProperty().bind(ebDerivedValueBinding);
            */

            // Working EasyBind Binding
            labelPlayground.textProperty().bind(
                    selectedItemBinding
                    .flatMap(ModelItem::detailsProperty)
                    .map(l -> derivedBinding(l))
                    .flatMap(l -> EasyBind.combine(l, stream -> stream.collect(Collectors.joining(", "))))
                    );

            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    private ObservableList<ObservableValue<String>> derivedBinding(ObservableList<ModelItemDetail> l) { 
        return l.stream()
                .map(c -> derivedBinding(c.aProperty(), c.bProperty()))
                .collect(Collectors.toCollection(FXCollections::observableArrayList));
    }

    private Binding<String> derivedBinding(ObservableValue<String> someA, ObservableValue<String> someB ) { 
        return EasyBind.combine(someA, someB, checkShowMore.selectedProperty(), 
                        (a, e, s) -> a + (Boolean.TRUE.equals(s) ? " <" + e + ">" : ""));
    }

    private ModelItem newModelItem(int number) { 
        ModelItem item = new ModelItem();
        item.itemNumber = number+1;
        for (int i=0;i<2;i++) { 
            ModelItemDetail detail = new ModelItemDetail("A" + (i+item.itemNumber), "B" + (i+item.itemNumber));
            item.getDetails().add(detail);
        }
        return item;
    }

    /** GUI Model class */ 
    private static class MasterDetailModel {
        private ObjectProperty<ModelItem> selectedItemProperty = new SimpleObjectProperty<>();
        public ObjectProperty<ModelItem> selectedItemProperty() { return selectedItemProperty; }
        public ModelItem getSelectedItem() { return selectedItemProperty.getValue(); }
        public void setSelectedItem(ModelItem item) { selectedItemProperty.setValue(item); }
    }

    /** Domain Model class */
    private static class ModelItem { 
        int itemNumber;
        private ListProperty<ModelItemDetail> detailsProperty = new SimpleListProperty<>(FXCollections.observableArrayList());
        public ListProperty<ModelItemDetail> detailsProperty() { return detailsProperty; }
        public ObservableList<ModelItemDetail> getDetails() { return detailsProperty.getValue(); }
        public void setDetails(List<ModelItemDetail> details) { detailsProperty.setValue(FXCollections.observableList(details)); }
        public String toString() { return "Item " + itemNumber; }
    }

    /** Domain Model class */
    private static class ModelItemDetail {

        public ModelItemDetail(String a, String b) { 
            setA(a);
            setB(b);
        }

        private StringProperty aProperty = new SimpleStringProperty();
        public StringProperty aProperty() { return aProperty; }
        public String getA() { return aProperty.get(); }
        public void setA(String a) { aProperty.set(a); }

        private StringProperty bProperty = new SimpleStringProperty();
        public StringProperty bProperty() { return bProperty; }
        public String getB() { return bProperty.get(); }
        public void setB(String b) { bProperty.set(b); }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM