如何处理相关的数据绑定?

joh*_*384 6 binding javafx-2

我经常发现自己遇到了控件的两个(相关)值得到更新的问题,并且两者都会触发昂贵的操作,或者控件可能会暂时处于不一致状态.

例如,考虑一个数据绑定,其中两个值(x,y)相互减去,最终结果用作某些其他属性z的除数:

z /(x - y)

如果x和y绑定到某个外部值,那么一次更新一个可能导致意外除零错误,具体取决于首先更新哪个属性以及另一个属性的旧值.更新属性z的代码只是监听x和y中的更改 - 它无法事先知道另一个属性的另一个更新.

现在这个问题很容易避免,但还有其他类似的情况,比如设置宽度和高度...我是否立即调整窗口大小或等待另一个更改?我是否立即为指定的宽度和高度分配内存,还是等待?如果宽度和高度是1和1百万,然后更新到100万和1,那么暂时我的宽度和高度为100万x 100万...

这可能是一个相当普遍的问题,虽然对我来说特别适用于JavaFX Bindings.我对如何处理这些情况感兴趣,而不会遇到未定义的行为或执行昂贵的操作,只要另一个绑定发生变化就需要重做.

到目前为止我为避免这些情况所做的事情是在设置新值之前首先清除绑定到已知值,但这对代码更新它真正不应该知道的绑定负担.

Nik*_*los 1

我现在才开始学习 JavaFX,所以对这个答案持保留态度......欢迎任何更正。我自己对此很感兴趣,所以做了一些研究。

失效监听器

这个问题的答案部分是InvalidationListener. 您可以在此处详细阅读文档,但本质是 aChangeLister立即传播更改,而 aInvalidationListener会注意到某个值无效,但会推迟计算直到需要时为止。演示基于“z / (x - y)”计算的两种情况的示例:

首先是一些琐碎的事情:

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableNumberValue;
import javafx.beans.value.ObservableValue;

public class LazyExample
{
    public static void main(String[] args) {
        changeListenerCase();
        System.out.println("\n=====================================\n");
        invalidationListenerCase();
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

两种情况(更改和失效侦听器)将设置 3 个变量:xyz、计算表达式z / (x - y)和相应的侦听器。然后他们调用一个manipulate()方法来更改值。记录所有步骤:

    public static void changeListenerCase() {
        SimpleDoubleProperty x = new SimpleDoubleProperty(1);
        SimpleDoubleProperty y = new SimpleDoubleProperty(2);
        SimpleDoubleProperty z = new SimpleDoubleProperty(3);

        NumberBinding nb = makeComputed(x,y,z);

        nb.addListener(new ChangeListener<Number>() {
            @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                System.out.println("ChangeListener: " + oldValue + " -> " + newValue);
            }
        });

        // prints 3 times, each after modification
        manipulate(x,y,z);

        System.out.println("The result after changes with a change listener is: " + nb.doubleValue());
    }

    public static void invalidationListenerCase() {
        SimpleDoubleProperty x = new SimpleDoubleProperty(1);
        SimpleDoubleProperty y = new SimpleDoubleProperty(2);
        SimpleDoubleProperty z = new SimpleDoubleProperty(3);

        NumberBinding nb = makeComputed(x,y,z);

        nb.addListener(new InvalidationListener() {
            @Override public void invalidated(Observable observable) {
                System.out.println("Invalidated");
            }
        });

        // will print only once, when the result is first invalidated
        // note that the result is NOT calculated until it is actually requested
        manipulate(x,y,z);

        System.out.println("The result after changes with an invalidation listener is: " + nb.doubleValue());
    }
Run Code Online (Sandbox Code Playgroud)

以及常用的方法:

    private static NumberBinding makeComputed(final ObservableNumberValue x, final ObservableNumberValue y, final ObservableNumberValue z) {
        return new DoubleBinding() {
            {
                bind(x,y,z);
            }
            @Override protected double computeValue() {
                System.out.println("...CALCULATING...");
                return z.doubleValue() / (x.doubleValue()-y.doubleValue());
            }
        };
    }

    private static void manipulate(SimpleDoubleProperty x, SimpleDoubleProperty y, SimpleDoubleProperty z) {
        System.out.println("Changing z...");
        z.set(13);
        System.out.println("Changing y...");
        y.set(1);
        System.out.println("Changing x...");
        x.set(2);
    }
Run Code Online (Sandbox Code Playgroud)

其输出是:

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableNumberValue;
import javafx.beans.value.ObservableValue;

public class LazyExample
{
    public static void main(String[] args) {
        changeListenerCase();
        System.out.println("\n=====================================\n");
        invalidationListenerCase();
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

所以第一种情况存在过多的计算和案例infinity。在第二种情况下,数据在第一次更改时被标记为无效,然后仅在需要时重新计算。

脉搏

绑定图形属性怎么样,例如某物的宽度和高度(如您的示例中)?看来 JavaFX 的基础设施不会立即应用图形属性的更改,而是根据称为Pulse 的信号。脉冲是异步调度的,执行时将根据节点属性的当前状态更新 UI。动画中的每一帧和 UI 属性的每次更改都将安排一个脉冲运行。

我不知道在您的示例中会发生什么,初始宽度=1px,高度=10 6 px,代码设置宽度=10 6 px(一步,调度脉冲),然后高度=1px(第二步)。如果第一个脉冲尚未处理,第二个步骤是否会发出另一个脉冲?从 JavaFX 的角度来看,合理的做法是管道仅处理 1 个脉冲事件,但我需要一些参考。但是,即使处理两个事件,第一个事件也应该处理整个状态更改(宽度和高度),以便在一个视觉步骤中发生变化。

我认为开发人员必须了解架构。假设有一个单独的任务(伪代码):

width = lengthyComputation();
Platform.runLater(node.setWidth(width));
height = anotherLengthyComputation();
Platform.runLater(node.setHeight(height));
Run Code Online (Sandbox Code Playgroud)

如果第一个脉冲事件有机会运行,那么用户将看到宽度的变化 - 暂停 - 高度的变化。最好将其写为(同样,始终在后台任务中)(伪代码):

width = lengthyComputation();
height = anotherLengthyComputation();
Platform.runLater(node.setWidth(width));
Platform.runLater(node.setHeight(height));
Run Code Online (Sandbox Code Playgroud)

更新(来自 john16384 的评论):根据此,不可能直接收听脉冲。然而,我们可以扩展某些方法,javafx.scene.Parent每个脉冲运行一次并达到相同的效果。layoutChildren()因此,如果不需要更改子树,则可以扩展,如果子树将被修改,则可以选择computePrefHeight(double width)/ 。computePrefWidth(double height)