Java视图的可用性属性超出了GUI视图的范围

时间:2014-12-05 08:54:25

标签: javafx

在使用JavaFX(Java8)工作一段时间后,我发现Properties的概念非常有用,允许使用bean兼容变量绑定更新使用计算树的更改,例如:

class Person {
    StringProperty name;
    ...
}

Person owner;
Person human;

owner.name().bind(human.name());

允许将GUI控件绑定到“模型”,以便在更改时自动更新。

所以我也开始在模型中使用Property<T>类(我正在进行功能操作的数据对象)。但JavaFX是一个单线程GUI实现,只有在JavaFX线程中完成时,才允许设置链接到某些GUI控件的属性。否则将抛出异常:

  Exception in thread "Thread-5" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-5

如果我现在开始编写多线程代码,我终于无法使用这些属性,即使我愿意。我无法在Platform.runLater()调用中封装每个更改以将其传递给JavaFX线程。

为什么JavaFX不提供线程安全的Property-Binding? (或者是吗?)

3 个答案:

答案 0 :(得分:33)

  

Java视图属性超出GUI视图范围的可用性

JavaFX属性绝对可以在GUI视图的范围之外使用。基本Oracle JavaFX binding tutorial通过创建一个简单的非GUI java程序来演示这一点,该程序代表一个Bill对象,其中账单的总金额通过JavaFX属性公开。

  

为什么JavaFX不提供线程安全的Property-Binding? (或者是吗?)

JavaFX不支持线程安全的属性绑定。可能它不支持线程安全属性绑定的原因是因为它不需要。

虽然JavaFX内部具有多线程体系结构,其中包含渲染线程,应用程序线程等,但在开发人员外部,它实际上只暴露单个应用程序线程。从开发人员的角度来看,开发人员将他们的应用程序编码为单线程系统。开发人员和内部JavaFX实现可以假设一切都在单线程环境中运行,并且编码更简单(有关为何会出现这种情况的背景信息,请参阅Multithreaded toolkits: A failed dream?How to run two JavaFX UI in actual different threads)。知道它正在这样的环境中执行的属性实现也可以采用单线程架构并跳过可能使内置线程安全控件复杂化。

JavaFX确实能够为concurrent tasks生成新线程(并且还可以使用标准Java开发的许多并发工具中的任何一个)。 JavaFX并发API确实能够以线程安全的方式反馈属性值(例如为任务执行完成的工作的百分比),但这是以非常特定的方式完成的。任务API具有用于修改此类属性的专用方法(例如updateProgress),并且在内部使用线程检查和调用(例如Platform.runLater)以确保代码以线程安全的方式执行。

因此,JavaFX并发实用程序不能通过JavaFX属性机制的内置功能来实现线程安全,而是通过在并发实用程序实现中对一组非常特定且有限的属性进行显式检查和调用。即使这样,用户在使用这些实用程序时也必须非常小心,因为他们通常认为他们可以直接从并发任务中修改JavaFX GUI阶段属性(即使文档声明不应该这样做);如果使用java.util.concurrent包等标准Java并发实用程序而不是javafx.concurrent,则需要同样谨慎。

有人可能会创建JavaFX属性机制的扩展,以便在线程安全环境中更好地运行,但是到目前为止还没有人创建过这样的扩展。

  

但JavaFX是单线程GUI实现,只有在JavaFX线程中完成时才允许设置链接到某些GUI控件的属性。

问题的标题是&#34; Java视图范围之外的JavaFX属性的可用性&#34;,但您描述的特定问题是这个的一个非常特定的子集,我认为您有以下组合事情:

  1. 模型类的属性。
  2. 模型类中的属性可以由JavaFX应用程序线程或其他用户线程读取和写入。
  3. 模型类中的属性可以绑定到活动的GUI元素,例如TextField中的文本或屏幕上显示的标签。
  4. 开箱即用,这是不可行的,因为JavaFX系统假定只在JavaFX应用程序线程上读取或修改JavaFX GUI组件的属性。此外,在内部支持绑定和更改侦听器,属性本身需要维护要修改的侦听器和绑定属性的列表,并且这些列表可能假设它们只能从单个线程访问或修改。您可以通过使用Platform.runLater包装调用来将调用放在JavaFX应用程序线程上来确保每个读取或写入都在单个线程上进行解决,但是根据您的问题,这正是代码的类型。你试图避免。

    IMO对于我在上述几点中概述的用例,没有其他解决方案,必须使用Platform.runLater包装。您可以通过为属性访问和更新提供Facade方法(类似于JavaFX并发Task实现)来隐藏runLater调用的一些复杂性和样板,但是这样的系统实现起来可能有点复杂(特别是如果您想要为整个属性/绑定子系统实现通用解决方案,而不是像Task这样的特定属性的专用解决方案。

    那么什么是JavaFX属性?

    主要的现有用例是支持JavaFX GUI应用程序的基于绑定的GUI编程模型。 JavaFX属性广泛用于JavaFX API和使用该API的任何应用程序。

    由于JavaFX现在包含在所有标准的新Oracle JDK发行版中,您还可以使用非JavaFX程序中的属性。例如,有关如何在JPA entity beans for instance中使用这些属性的讨论。根据我的经验,这些非JavaFX API用例目前非常罕见。

    虽然JavaFX属性和绑定包位于javafx包命名空间内,但它们不依赖于其他JavaFX包。在未来的模块化JDK(例如Java 9)中,有可能让Java程序依赖于JavaFX属性和绑定模块,并且不依赖于其他模块来进行JavaFX GUI开发(这可能对某些无头服务器系统,它们是许多Java应用程序的主要部署目标。

    最初为其他JavaFX工具设置了类似的设计,例如时间轴和转换,这样通过时间轴基于时间的任务调度的实时Java系统可以使用来自JavaFX的动画/时间轴模块,而不依赖于其余的到JavaFX系统(但我不确定原始设计是否已经延续到今天,因此可能不再可能实现,动画模块的基本脉冲通常键入60fps的最小刻度,可能锁定在屏幕刷新率取决于实现。)

    JavaFX属性不是Java的属性管理的通用解决方案,但它们可能是迄今为止我见过的最接近,最完整的解决方案。理想的(我记得JavaFX项目负责人Richard Bair所说的)是,属性功能将构建到Java编程语言中,因此支持不仅来自API,还来自改进的语言语法。也许某些未来的Java版本(如10+)可能具有这些设施。当然,这是一个古老的讨论,可能追溯到Java语言和JDK规范的开头。尽管如此,世界并不理想,JavaFX属性机制(即使它具有庞大的语法和缺乏内置的线程安全支持)仍然是许多应用程序的有用工具。另请注意,Scala通过ScalaFX可以扩展其他语言,这使得JavaFX属性机制看起来像是该语言语法的一部分。

    第三方库(如EasyBind)增强了JavaFX属性机制,以更好地支持编程范例,例如功能反应式编程。

    目前,如果我想在JavaFX程序中广泛使用属性类型工具,我可能会将它基于JavaFX属性和(可能)EasyBind和ReactFX的组合,因为它似乎是Java的最佳解决方案。

    我不会在高度并发的环境中使用这样的解决方案,因为底层库中缺少线程安全支持,因此线程非划分。属性基于对可变对象的副作用更改,这在多线程程序中是很难推理的。即使修改了内部属性实现以允许对属性进行线程安全读/写,我也不确定它是否会是一个很好的方法。对于需要在并发子任务之间进行大量通信的高度并发系统,使用其他编程范例(例如Actors(例如akka / erlang)或communicating sequential processes可能比绑定属性。

答案 1 :(得分:0)

我今天碰到了这个问题,有人指出线程确实很有帮助。由此引起的错误可能是阴险的。

我正在广泛使用这个解决方案。您不能使用ThreadSafeObjectProperty绑定双向,但您可以绑定到FX属性并使用ThreadSafePropertySetter更新它。

setter返回Future。这可用于控制由Platform.runLater引起的竞争条件。

这是在Scala:

class SafePublishProperty[T](init: T) {
  val writable = new ReadOnlyObjectWrapper[T](init)
  def readOnly: ObjectExpression[T] = writable.getReadOnlyProperty
}

class ThreadSafeBooleanProperty(init: Boolean) {
  protected val property = new ReadOnlyBooleanWrapper(init)

  def value: BooleanExpression = property.getReadOnlyProperty

  def setValue(value: Boolean): Future[Boolean] = {
    val promise = Promise[Boolean]
    if (Platform.isFxApplicationThread) {
      property.setValue(value)
      promise.success(true)
    }
    else
      try {
        Platform.runLater(() => {
          property.setValue(value)
          promise.success(true)
        })
      } catch {
        case _: IllegalStateException =>
          property.setValue(value)
          promise.success(true)
      }
    promise.future
  }
}

class ThreadSafeObjectProperty[T](init: T) {
  protected val property = new SafePublishProperty[T](init)

  def value: ObjectExpression[T] = property.readOnly

  def setValue(value: T): Future[Boolean] = {
    val promise = Promise[Boolean]
    if (Platform.isFxApplicationThread) {
      property.writable.setValue(value)
      promise.success(true)
    }
    else {
      try {
        Platform.runLater(() => {
          property.writable.setValue(value)
          promise.success(true)
        })
      } catch {
        case _: IllegalStateException =>
          property.writable.setValue(value)
          promise.success(true)
      }
    }
    promise.future
  }
}

object ThreadSafePropertySetter {
  def execute(function: () => Unit): Future[Boolean] = {
    val promise = Promise[Boolean]
    if (Platform.isFxApplicationThread) {
      function.apply()
      promise.success(true)
    }
    else {
      try {
        Platform.runLater(() => {
          function.apply()
          promise.success(true)
        })
      } catch {
        case ex: IllegalStateException =>
          function.apply()
          promise.success(true)
      }
    }
    promise.future
  }
}

典型用法:

  class SomeExample {
    private val propertyP = new ThreadSafeBooleanProperty(true)

    def property: BooleanExpression = propertyP.value

    private class Updater extends Actor {
      override def receive: Receive = {
        case update: Boolean =>
          propertyP.setValue(update)
      }
    }
  }

答案 2 :(得分:0)

使用更改侦听器而不是绑定。这是一个很好的实用方法:

public <T> ChangeListener<T> getFxListener(Consumer<T> consumer) {
    return (observable, oldValue, newValue) -> {
        if (Platform.isFxApplicationThread()) {
            consumer.accept(newValue);
        }
        else {
            Platform.runLater(() -> consumer.accept(newValue));
        }
    };
}

控制器中的用法示例:

// domain object with JavaFX property members
private User user;

@FXML
private TextField userName;

@FXML
protected void initialize() {
    user.nameProperty().addListener(getFxListener(this::setUserName));
}

public void setUserName(String value) {
    userName.setText(value);
}

我知道原始的发帖人提到他负担不起使用Platform.runLater,但没有说明原因。如果绑定是线程安全的,无论如何它们将在后台进行此调用。没有线程安全的捷径,这些调用是顺序进行的,因此可以安全使用。