JavaFX valueAt() Binding вычисляется только один раз

Мы знаем, что у ListExpression есть метод ObjectBinding<E> valueAt(ObservableIntegerValue). Мы можем использовать этот метод для точного прослушивания элемента ListProperty.

Я ожидаю, что он будет привязан как к ListProperty, так и к ObservableNumberValue. Таким образом, либо список изменений, либо изменения наблюдаемого числового значения сделают привязку недействительной и перевычислят. Но в следующем коде привязка вычисляется только один раз! (На самом деле дважды, если мы не игнорируем начальное вычисление)

Метка будет отображать случайную строку в начале. И property будет иметь 100 бобов в качестве начального значения. Если мы нажмем button1, indexProperty увеличится на 1. Если мы нажмем button2, Bean, расположенный в текущий индекс ListProperty, изменится. Оба эффекта делают привязку недействительной и пересчитывают текст метки.

Но на практике текст изменится при первом нажатии одной кнопки. И больше не изменится.

Я использую Liberica JDK17, который по умолчанию содержит jmods JavaFX.

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, код будет работать так, как мы ожидали.

В моей программе привязка вернет свойство Bean в местоположении indexProperty.получить() из свойство.


Изменить (от 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);
    }
}

Выезд: bugs.openjdk.java.net/browse/JDK-8273138

Slaw 28.03.2022 01:03
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
1
86
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я собираюсь ответить на Java, так как я лучше знаком с ней. Это рассуждение применимо и к котлину.

Bindings.createStringBinding(function, dependencies) создает привязку, которая становится недействительной каждый раз, когда любая из зависимостей становится недействительной. Здесь «недействительный» означает «переходит из действительного состояния в недопустимое состояние». Проблема с вашим кодом заключается в том, что вы определяете зависимость как property.valueAt(index), которая является привязкой, на которую у вас нет другой ссылки.

Когда вы изменяете либо индекс, либо компонент по этому индексу в списке, привязка становится недействительной. Поскольку вы больше никогда не вычисляете значение этой привязки, он никогда не станет действительным снова (т. е. оно никогда не содержит или не возвращает допустимое значение). Таким образом, последующие изменения не изменят его состояние проверки (он просто все еще недействителен; он не может перейти из действительного в недействительное).

Изменение кода на

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.

Я думаю, что перевод этого котлина


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 17.03.2022 16:02

Это совершенно правильно @James_D. Свойства внутри свойств, индексированных свойством через valueAt(), становятся довольно дикими. Ключевым моментом здесь является то, что вам нужно иметь get() в вычислении значения для каждого Observable, используемого в качестве триггера для Binding. Kotlin неплох, но вы должны использовать «value» вместо «get» (или даже get()).

DaveB 17.03.2022 18:48

@DaveB Спасибо за подсказку Kotlin; это имеет смысл. Я отредактировал этот блок кода.

James_D 17.03.2022 18:52

@DaveB Я был бы осторожен с использованием property.value, когда свойство оборачивает примитивный тип, потому что этот синтаксис приведет к вызову метода property.getValue(), который определен для возврата типа в штучной упаковке. Я сомневаюсь, что в противном случае это приведет к значительному снижению производительности в большинстве случаев, но использование .get() гарантирует, что примитивный тип не упакован (по крайней мере, в этом месте кода).

Slaw 17.03.2022 23:45

@Слав, согласен. Лично я ненавижу иметь дело с числом, поэтому я начал использовать ObjectProperty<Int> или ObjectProperty<Double> вместо IntegerProperty или DoubleProperty. Я думаю, что это делает проблему getValue() и get() в основном спорной. Мне нужно взглянуть на источник TornadoFX, чтобы увидеть, как они справляются с такими вещами.

DaveB 18.03.2022 15:15

Для будущих читателей, проверьте: bugs.openjdk.java.net/browse/JDK-8273138

Slaw 28.03.2022 01:03

Благодаря @James_D я не только знаю, почему этот код не работал должным образом, но и решил еще одну проблему, связанную с Bindings.

TL;DR - JavaFX не будет автоматически проверять привязки, когда зависимости становятся недействительными.. Вы должны проверить их, получив значение или как-то еще.

В следующем коде bean станет недействительным либо property, либо index изменится, а StringBinding будет вычислено. После вычисления бин по-прежнему недействителен, потому что мы не вычисляли его для проверки. Поэтому в следующий раз, когда зависимости bean изменятся, StringBinding не будет пересчитываться.

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

Но для этого все иначе. Хотя мы никогда не используем значение привязки bean, мы проверяем его, получая. Таким образом, код работает так, как ожидалось.

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

И для другой ситуации, будьте осторожны! Май bean стал недействительным, и во время вычислений flag было false, поэтому beanне подтвердил. Если мы не проверим bean каким-либо образом где-нибудь до следующего раза, зависимости bean снова станут недействительными, привязка не будет вычисляться.

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

Мне любопытно, потому что вы, похоже, не используете свойства в Bean как свойства. Кроме того, использование привязки, когда вы не собираетесь использовать какие-либо связанные свойства триггера, кажется чем-то вроде запаха кода. Не было бы более очевидным/прямым просто использовать здесь InvalidationListener?

DaveB 18.03.2022 15:19

Ага, InvalidationListener! Я даже этого не заметил. Спасибо за совет. :)

Meodinger Wang 19.03.2022 11:12

Другие вопросы по теме