Мы знаем, что у 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.получить() из свойство.
Чтобы увеличить аудиторию для этого вопроса, вот (насколько мне известно) перевод этого примера на 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)
, которая является привязкой, на которую у вас нет другой ссылки.
Когда вы изменяете либо индекс, либо компонент по этому индексу в списке, привязка становится недействительной. Поскольку вы больше никогда не вычисляете значение этой привязки, он никогда не станет действительным снова (т. е. оно никогда не содержит или не возвращает допустимое значение). Таким образом, последующие изменения не изменят его состояние проверки (он просто все еще недействителен; он не может перейти из действительного в недействительное).
Изменение кода на
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. Свойства внутри свойств, индексированных свойством через valueAt(), становятся довольно дикими. Ключевым моментом здесь является то, что вам нужно иметь get() в вычислении значения для каждого Observable, используемого в качестве триггера для Binding. Kotlin неплох, но вы должны использовать «value» вместо «get» (или даже get()).
@DaveB Спасибо за подсказку Kotlin; это имеет смысл. Я отредактировал этот блок кода.
@DaveB Я был бы осторожен с использованием property.value
, когда свойство оборачивает примитивный тип, потому что этот синтаксис приведет к вызову метода property.getValue()
, который определен для возврата типа в штучной упаковке. Я сомневаюсь, что в противном случае это приведет к значительному снижению производительности в большинстве случаев, но использование .get()
гарантирует, что примитивный тип не упакован (по крайней мере, в этом месте кода).
@Слав, согласен. Лично я ненавижу иметь дело с числом, поэтому я начал использовать ObjectProperty<Int> или ObjectProperty<Double> вместо IntegerProperty или DoubleProperty. Я думаю, что это делает проблему getValue() и get() в основном спорной. Мне нужно взглянуть на источник TornadoFX, чтобы увидеть, как они справляются с такими вещами.
Для будущих читателей, проверьте: bugs.openjdk.java.net/browse/JDK-8273138
Благодаря @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?
Ага, InvalidationListener! Я даже этого не заметил. Спасибо за совет. :)
Выезд: bugs.openjdk.java.net/browse/JDK-8273138