JavaFX ListView Cell在单元构造函数中的graphic和itemProperty之间的双向绑定

时间:2017-01-23 18:42:03

标签: javafx javafx-8

我设法通过覆盖ListView来使updateItem(T item,boolean empty)工作,但现在我需要在ListView节点中显示与我的域对象数据双向链接的节点。使用您请求编辑模式的现有系统非常难以使用(例如,进入编辑模式会从TextField移除焦点,因此您无法通过选择TextField来输入焦点,这会强制我定义单元格或图形中的逻辑)。我正在尝试使用绑定来完成这项工作,但我需要知道何时应该添加绑定,删除它们,显示图形与否。

以下代码可以让您更好地了解我的需求,但它绝对不是好事或有效。我试图通过几种方式获得所需的结果,唯一的好结果是它确实与我的域及其逻辑正确链接。坏消息是每当我添加,删除或滚动该列表时,结果都是意外的,但绝对不能正常工作。

MyCell extends ListCell<MyModel> {
    private SomethingThatExtendsNode view = new SomethingThatExtendsNode();

    public MyCell(){
        BooleanBinding remove = itemProperty().isNull()or(emptyProperty());
        remove.addListener((observable, oldValue, newValue) -> {setGraphic(null)});
        BooleanBinding exists = itemProperty().isNotNull().and(emptyProperty().not);
        exists.addListener((observable, oldValue, newValue) -> {
            if (newValue){
                setGraphic(view);
                MonadicObservableValue<MyModel> model = EasyBind.monadic(itemProperty());
                view.getTextField1.textProperty().bindBidirectional(model.selectProperty(MyModel::text1));
                //etc..
            }
        });
    }
}

此外,这种设计是正确的方法吗?我希望我的观点只是让他们的属性绑定到域。业务逻辑主要通过更多绑定在域中。

编辑:尝试按照答案进行设置,但仍然行为不规律,也许问题必须在其他地方。 MCVE的Git在这里https://github.com/PopescuStefanRadu/JavaFX-listView-MCVE

此外,在文本字段

中提供输入时,此错误似乎时不时出现
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.scene.control.TextInputControl$TextProperty.fireValueChangedEvent(TextInputControl.java:1389)
    at javafx.scene.control.TextInputControl$TextProperty.markInvalid(TextInputControl.java:1393)
    at javafx.scene.control.TextInputControl$TextProperty.controlContentHasChanged(TextInputControl.java:1332)
    at javafx.scene.control.TextInputControl$TextProperty.access$1600(TextInputControl.java:1300)
    at javafx.scene.control.TextInputControl.lambda$new$162(TextInputControl.java:139)
    at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.scene.control.TextField$TextFieldContent.insert(TextField.java:87)
    at javafx.scene.control.TextInputControl.replaceText(TextInputControl.java:1204)
    at javafx.scene.control.TextInputControl.updateContent(TextInputControl.java:556)
    at javafx.scene.control.TextInputControl.replaceText(TextInputControl.java:548)
    at com.sun.javafx.scene.control.skin.TextFieldSkin.replaceText(TextFieldSkin.java:576)
    at com.sun.javafx.scene.control.behavior.TextFieldBehavior.replaceText(TextFieldBehavior.java:202)
    at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.defaultKeyTyped(TextInputControlBehavior.java:238)
    at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callAction(TextInputControlBehavior.java:139)
    at com.sun.javafx.scene.control.behavior.BehaviorBase.callActionForEvent(BehaviorBase.java:218)
    at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callActionForEvent(TextInputControlBehavior.java:127)
    at com.sun.javafx.scene.control.behavior.BehaviorBase.lambda$new$74(BehaviorBase.java:135)
    at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.Scene$KeyHandler.process(Scene.java:3964)
    at javafx.scene.Scene$KeyHandler.access$1800(Scene.java:3910)
    at javafx.scene.Scene.impl_processKeyEvent(Scene.java:2040)
    at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2501)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:217)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:149)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$353(GlassViewEventHandler.java:248)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:247)
    at com.sun.glass.ui.View.handleKeyEvent(View.java:546)
    at com.sun.glass.ui.View.notifyKey(View.java:966)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$null$49(GtkApplication.java:139)
    at java.lang.Thread.run(Thread.java:745)

2 个答案:

答案 0 :(得分:0)

您永远不应该在单元格构造函数中添加任何绑定或侦听器,因为每次必须显示新项目时,列表单元格一旦创建就会被列表视图重用。在构造函数中绑定属性时,当列表视图更改它时,您将无法从旧项中删除它。当你滚动或更改列表时,这就是你有奇怪行为的原因。

在更新自定义列表单元格中的项目之前,必须删除双向绑定和侦听器。请检查以下代码:

@Override
public void start(Stage primaryStage) {
    ObservableList<Model> myModels = FXCollections.observableArrayList(
            IntStream.range(0, 100).mapToObj(i -> new Model("MyModel " + i)).collect(Collectors.toList())
    );

    ListView<Model> modelListView = new ListView<>();
    modelListView.setCellFactory(c -> new MyListCell());
    modelListView.setItems(myModels);

    StackPane root = new StackPane();
    root.getChildren().addAll(modelListView);

    Scene scene = new Scene(root, 300, 250);

    primaryStage.setTitle("Custom list cell");
    primaryStage.setScene(scene);
    primaryStage.show();
}

private class MyListCell extends ListCell<Model> {

    private HBox content = new HBox();
    private TextField textField = new TextField();
    private Button actionButton = new Button("Action");

    private final ChangeListener<String> textListener = (ObservableValue<? extends String> observable, String oldValue, String newValue) -> {
        checkIfDisableButton(newValue);
    };

    public MyListCell() {
        content.getChildren().addAll(textField, actionButton);
        content.setSpacing(10);
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        setGraphic(content);
    }

    @Override
    protected void updateItem(Model item, boolean empty) {
        if (getItem() != null) { // get old item
            //remove all bidirectional bindings and listeners
            textField.textProperty().unbindBidirectional(getItem().nameProperty());
            getItem().nameProperty().removeListener(textListener);
        }
        super.updateItem(item, empty);
        //new item
        if (item == null || empty) {
            setText(null);
            setGraphic(null);
        } else {
            setGraphic(content);
            checkIfDisableButton(item.getName());
            item.nameProperty().addListener(textListener);
            textField.textProperty().bindBidirectional(item.nameProperty());
            actionButton.setOnAction(e -> {
                System.out.println(item.getName());
            });
        }
    }

    private void checkIfDisableButton(String value) {
        actionButton.setDisable("MyModel 2".equals(value));
    }
}

private class Model {

    StringProperty name = new SimpleStringProperty();

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

    public String getName() {
        return name.get();
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public StringProperty nameProperty() {
        return this.name;
    }

}

答案 1 :(得分:0)

问题解决了。

构建

ListView以便图形反映基础数据,因此无需将其绑定到模型。每当模型发生变化时,UI都会更新。

因此,我必须确保模型数据的更改使ObservableList了解更改。为此,我使用了一个提取器:

ObservableList<Model> myModels = FXCollections.observableArrayList(model -> new Observable[]{model.nameProperty()});

接下来我必须确保UI中的更改也会导致模型发生变化,因此我在cellFactory中使用了ChangeListener。这也可以放在扩展ListCell的类的构造函数中。我试图使用绑定但它们似乎不起作用,不知道为什么,稍后会调查它。我想在每一个属性上使用比听众更冗长的东西,但它仍然有用。

这是代码:

@Override
public void start(Stage primaryStage) throws Exception {
    ObservableList<Model> myModels = FXCollections.observableArrayList(model -> new Observable[]{model.nameProperty()});

    ListView<Model> modelListView = new ListView<>();
    modelListView.setCellFactory(c -> {
        MyListCell cell = new MyListCell();
        //TODO is there a way to replace this listener with something less verbose?
        cell.getTextField().textProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue!=null){
                cell.getItem().setName(newValue);
            }
        });

        return cell;
    });
    modelListView.setItems(myModels);
    myModels.add(new Model());

    Button addButton = new Button("Add");
    addButton.setOnAction(event -> myModels.add(new Model()));

    StackPane root = new StackPane();
    root.getChildren().addAll(new HBox(20, modelListView, addButton));
    primaryStage.setTitle("ListView with bidirectional binding");
    primaryStage.setScene(new Scene(root, 600, 500));
    primaryStage.show();
}

private class Model {
    private StringProperty name = new SimpleStringProperty(this, "name", "");
    private ReadOnlyStringWrapper computedName = new ReadOnlyStringWrapper(this, "computedName");

    public Model() {
        computedName.bind(Bindings.createStringBinding(() -> name.get().toUpperCase(), name));
    }

    public String getName() {
        return name.get();
    }

    public StringProperty nameProperty() {
        return name;
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public String getComputedName() {
        return computedName.get();
    }

    public ReadOnlyStringWrapper computedNameProperty() {
        return computedName;
    }
}

private class MyListCell extends ListCell<Model> {
    private HBox content = new HBox(10);
    private TextField textField = new TextField();
    private Label label = new Label();

    public MyListCell() {
        content.getChildren().addAll(textField, label);
    }

    @Override
    protected void updateItem(Model item, boolean empty) {
        super.updateItem(item, empty);
        if (item == null || empty) {
            setGraphic(null);
        } else {
            setGraphic(content);
            textField.textProperty().set(item.getName());
            label.textProperty().set(item.getComputedName());
        }
    }

    public TextField getTextField() {
        return textField;
    }

}