JavaFX valueAt() 绑定只计算一次

JavaFX valueAt() Binding only compute once

我们知道ListExpression有一个方法ObjectBinding<E> valueAt(ObservableIntegerValue)。我们可以使用这个方法准确地监听ListProperty的一个元素。

我预计它会绑定到 ListPropertyObservableNumberValue。因此,无论是列表更改还是可观察数值更改都会使绑定无效并重新计算。但在下面的代码中,绑定只计算一次! (实际上是两次,如果我们不忽略初始计算的话)

标签将在开头显示一个随机字符串。而 property 将有 100 个 Bean 作为初始值。如果我们点击button1indexProperty会增加1。如果我们点击button2,位于ListProperty当前索引的Bean将会改变。这两种效果都会使绑定无效并重新计算标签文本。

但实际上,当单击一个按钮时,文本会在第一次发生变化。也不会再变了。

我使用的是 Liberica JDK17,它默认包含 JavaFX 的 jmod。

class FixmeApp : Application() {

    companion object {
        fun genRandomDouble(): Double = Math.random() * 10000
        fun genRandomString(): String = genRandomDouble().roundToInt().toString(36)
    }

    class Bean {
        val stringProperty = SimpleStringProperty(genRandomString())
        val doubleProperty = SimpleDoubleProperty(genRandomDouble())
    }

    val property: ListProperty<Bean> = SimpleListProperty(FXCollections.observableArrayList(Bean()))

    override fun start(primaryStage: Stage) {
        property.addAll((0..100).map { Bean() })

        val indexProperty = SimpleIntegerProperty(0)

        val label = Label().apply {
            textProperty().bind(Bindings.createStringBinding(
                { genRandomString() },
                property.valueAt(indexProperty)
            ))
        }
        val button1 = Button("Change Index").apply {
            setOnAction {
                indexProperty.set(indexProperty.get() + 1)
            }
        }
        val button2 = Button("Change Bean").apply {
            setOnAction {
                property[indexProperty.get()] = Bean()
            }
        }
        val scene = Scene(BorderPane().apply {
            center = label
            bottom = HBox(button1, button2)
        })

        primaryStage.scene = scene
        primaryStage.show()

    }

}

fun main() {
    Application.launch(FixmeApp::class.java)
}

顺便说一句,如果我们将绑定依赖项从 property.valueAt(indexProperty) 更改为 property, indexProperty,代码将如我们预期的那样 运行。

在我的程序中,绑定将return Bean 的属性 位于indexProperty.get() 的位置 ]属性


编辑(James_D):

为了增加这个问题的听众,这里(据我所知)将这个例子翻译成 Java。

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.util.Random;

public class FixMeApp extends Application {

    private final Random rng = new Random();

    private final ListProperty<Bean> property = new SimpleListProperty<>(FXCollections.observableArrayList());

    @Override
    public void start(Stage stage) throws Exception {
        for (int i = 0 ; i < 100 ; i++) property.add(new Bean());
        var index = new SimpleIntegerProperty(0);
        var label =  new Label();
        label.textProperty().bind(Bindings.createStringBinding(
                this::generateRandomString,
                property.valueAt(index)
        ));
        var button1 = new Button("Change Index");
        button1.setOnAction(e -> index.set(index.get() + 1));
        var button2 = new Button("Change bean");
        button2.setOnAction(e -> property.set(index.get(), new Bean()));
        var root = new BorderPane(label);
        root.setBottom(new HBox(button1, button2));
        var scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    private double generateRandomDouble() {
        return rng.nextDouble() * 10000 ;
    }

    private String generateRandomString() {
        return Integer.toString((int) generateRandomDouble());
    }

    class Bean {
        private StringProperty stringProperty = new SimpleStringProperty(generateRandomString());
        private DoubleProperty doubleProperty = new SimpleDoubleProperty(generateRandomDouble());

        StringProperty stringProperty() { return stringProperty; }
        DoubleProperty doubleProperty() { return doubleProperty; }
    }

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

我会在 Java 中回复,因为我比较熟悉它。推理也适用于科特林。

Bindings.createStringBinding(function, dependencies) 创建一个绑定,只要任何依赖项失效,该绑定就会失效。这里的“失效”是指“从有效状态变为无效状态”。您的代码的问题是您将依赖项定义为 property.valueAt(index),这是一个您没有其他引用的绑定。

当您更改列表中的索引或该索引处的 bean 时,绑定将失效。由于您再也不会计算该绑定的值,它再也不会变得有效(即它永远不会持有或返回有效值)。因此后续更改不会更改其验证状态(它只是仍然无效;它无法从有效转换为无效)。

将代码更改为

label.textProperty().bind(Bindings.createStringBinding(
        () -> property.valueAt(index).get().stringBinding().get(),
        property.valueAt(index)
));

没有帮助:您有一个用于依赖项的绑定(其值永远不会计算,因此它永远不会返回到有效状态)并且每次计算值时都有不同的绑定。永远不会计算用作依赖项的绑定的值,因此它永远不会返回到有效状态,因此永远不会再次失效。

但是

ObjectBinding<Bean> bean = property.valueAt(index);
label.textProperty().bind(Bindings.createStringBinding(
        () -> bean.get().stringProperty().get(),
        bean
));

会起作用。这里文本的计算强制 bean 计算其当前值,将其返回到有效状态。然后对索引或列表的后续更改将再次使 bean 无效,从而触发重新计算绑定 text 的绑定。

我认为这是 kotlin 翻译


val bean = property.valueAt(indexProperty)
val label = Label().apply {
    textProperty().bind(Bindings.createStringBinding(
        { bean.value.stringProperty.value },
        bean
    ))
}

您可以尝试其他变体。这个不会更新超过一次,因为 bean 从未经过验证:

ObjectBinding<Bean> bean = property.valueAt(index);
label.textProperty().bind(Bindings.createStringBinding(
        this::generateRandomString,
        bean
));

而这个强制验证,所以它总是更新:

ObjectBinding<Bean> bean = property.valueAt(index);
label.textProperty().bind(Bindings.createStringBinding(
        () -> {
            bean.get();
            return generateRandomString();
        },
        bean
));

感谢@James_D,我不仅知道为什么这段代码没有如预期的那样运行而且还解决了另一个与Bindings相关的问题

TL;DR - 当依赖项变得无效时,JavaFX 不会自动验证绑定。您应该通过获取值或以某种方式验证它们。

在下面的代码中,bean 将在 propertyindex 发生变化时失效,并且 StringBinding 将进行计算。计算之后,bean 仍然无效 因为我们没有计算验证它。所以下次 bean 的依赖关系改变时, StringBinding 将不会重新计算。

ObjectBinding<Bean> bean = property.valueAt(index);
label.textProperty().bind(Bindings.createStringBinding(
        this::generateRandomString,
        bean
));

但对于这个,它是不同的。虽然我们从不使用绑定 bean 的值,但我们通过获取来验证它。所以代码 运行s 符合预期。

ObjectBinding<Bean> bean = property.valueAt(index);
label.textProperty().bind(Bindings.createStringBinding(
        () -> {
            bean.get();
            return generateRandomString();
        },
        bean
));

还有一种情况,要小心!可能 bean 变得无效并且在计算期间,flagfalse 所以 bean 没有验证 。如果我们在下次 bean 的依赖项再次无效之前不以某种方式验证 bean 绑定将不会计算

ObjectBinding<Bean> bean = property.valueAt(index);
label.textProperty().bind(Bindings.createStringBinding(
        () -> {
            if (flag) return bean.value 
            else return generateRandomString();
        },
        bean
));