Удобство использования свойств JavaFX за пределами видимости GUI

Поработав некоторое время с JavaFX (Java8), я нашел очень полезной концепцию Properties, позволяющую использовать переменные, совместимые с bean-компонентами, для привязки к обновлениям с использованием дерева вычислений, например:

class Person {
    StringProperty name;
    ...
}

Person owner;
Person human;

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

Это позволяет привязать элементы управления GUI к "модели", чтобы автоматически обновляться при изменении.

Поэтому я также начал использовать класс Property<T> в модели (мои объекты данных я выполняю своими функциональными операциями). Но JavaFX - это однопоточная реализация GUI, и установка такого свойства, связанного с некоторыми элементами управления GUI, допускается только в том случае, если это выполняется в потоке JavaFX. В противном случае будет выдано исключение:

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

Если я сейчас начну писать многопоточный код, я, наконец, не смогу использовать эти свойства, даже если бы захотел. Я не могу позволить себе инкапсулировать каждое изменение в вызове Platform.runLater() чтобы передать его потоку JavaFX.

Почему JavaFX не обеспечивает поточно-привязанную привязку свойств? (Или это?)

Ответ 1

Удобство использования свойств JavaFX вне области графического интерфейса

Свойства JavaFX могут определенно использоваться вне области представления GUI. Базовое руководство по связыванию Oracle JavaFX демонстрирует это путем создания простой Java-программы без графического интерфейса пользователя, которая представляет объект Bill, в котором общая сумма счета выставляется через свойство JavaFX.

Почему JavaFX не обеспечивает поточно-привязанную привязку свойств? (Или это?)

JavaFX не поддерживает потокобезопасное связывание свойств. Вероятно, причина, по которой он не поддерживает привязку потоковых свойств, заключается в том, что это не нужно.

Хотя JavaFX внутренне имеет многопоточную архитектуру с потоком рендеринга, потоком приложения и т.д., Внешне для разработчиков он действительно предоставляет только один поток приложения. С точки зрения разработчика, разработчик кодирует свое приложение как однопоточную систему. Разработчик и внутренние реализации JavaFX могут просто предположить, что все выполняется в однопоточной среде, и кодирование намного проще (см. Многопоточные наборы инструментов: несостоявшаяся мечта? И Как запустить два пользовательского интерфейса JavaFX в реальных разных потоках для некоторой справочной информации о почему это так). Реализация свойства, зная, что оно выполняется в такой среде, также может принимать однопоточную архитектуру и пропускать потенциально усложняющие встроенные элементы управления безопасностью потоков.

JavaFX имеет возможность порождать новые потоки для одновременных задач (и любое из множества одновременных средств для стандартной разработки Java также может использоваться). Параллельный API JavaFX действительно имеет возможность возвращать значения свойств (например, процент работы, выполненной для выполнения задачи) потокобезопасным способом, но это делается очень специфическим способом. API задачи имеет специальные методы для изменения таких свойств (например, updateProgress), и внутренне он использует проверки потоков и вызовы, такие как Platform.runLater чтобы обеспечить выполнение кода потокобезопасным способом.

Таким образом, утилиты параллелизма JavaFX не обеспечивают безопасность потоков благодаря встроенным возможностям механизма свойств JavaFX, а вместо этого - посредством явных проверок и вызовов внутри реализации утилиты параллелизма для очень специфического и ограниченного набора свойств. Даже в этом случае пользователи должны быть очень осторожны при использовании этих утилит, поскольку они часто предполагают, что могут делать такие вещи, как изменение свойств этапа JavaFX GUI непосредственно из своих параллельных задач (даже если в документации указано, что этого не следует делать); javafx.concurrent же самое необходимо предпринять, если вместо javafx.concurrent используются стандартные утилиты Java, такие как пакет java.util.concurrent.

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

Но JavaFX - это однопоточная реализация GUI, и установка такого свойства, связанного с некоторыми элементами управления GUI, допускается только в том случае, если это выполняется в потоке JavaFX.

Название вопроса - "Удобство использования свойств JavaFX вне области представления GUI", но конкретная проблема, которую вы описываете, представляет собой довольно специфический подмножество этого, и я думаю, что у вас есть следующая комбинация вещей:

  1. Свойства на модельном классе.
  2. Свойства в классе модели могут быть прочитаны или записаны потоком приложения JavaFX или другим потоком пользователя.
  3. Свойства в классе модели могут быть связаны с активными элементами графического интерфейса, такими как текст в TextField или метка, отображаемая на экране.

Из коробки это не сработает, потому что система JavaFX предполагает, что свойства компонентов JavaFX GUI читаются или изменяются только в потоке приложения JavaFX. Кроме того, внутренне для поддержки связывания и изменения прослушивателей самим свойствам необходимо поддерживать списки прослушивателей и связанные свойства для изменения, и эти списки, вероятно, предполагают, что они будут доступны или изменены только из одного потока. Возможно, вы можете обойти эту проблему, обеспечив, чтобы каждое чтение или запись выполнялось в одном потоке, заключая вызовы в вызовы с помощью Platform.runLater для выполнения вызова в потоке приложения JavaFX, но, исходя из вашего вопроса, это именно тот код, которым вы являетесь пытаясь избежать.

IMO для варианта использования, который я обрисовал в общих чертах выше, другого решения не существует, и необходимо использовать упаковку Platform.runLater. Вы можете потенциально скрыть некоторые сложности и шаблон для вызовов runLater, предоставляя фасадные методы для доступа к свойствам и обновлениям (аналогично одновременной реализации Задачи JavaFX), но такая система, вероятно, немного сложна для реализации (особенно если вы хотите получить общее решение для всей подсистемы свойств/привязок, а не специализированное решение для пары конкретных свойств, таких как Task).

Каковы свойства JavaFX для тогда?

Основной существующий вариант использования предназначен для поддержки модели программирования на основе привязки GUI для приложений JavaFX GUI. Свойства JavaFX широко используются с API JavaFX и любым приложением, которое использует этот API.

Поскольку JavaFX теперь включен во все стандартные новые дистрибутивы Oracle JDK, вы также можете использовать свойства программ, не относящихся к JavaFX. Например, есть дискуссии о том, как использовать эти свойства, например, в бинах сущностей JPA. Эти случаи использования не-JavaFX API в настоящее время довольно редки в моем опыте.

Хотя свойства JavaFX и пакеты привязки находятся в пространстве имен пакетов javafx, они не зависят от других пакетов JavaFX. В будущем модульном JDK, таком как Java 9, предполагается, что будет возможно иметь программу Java, зависящую от свойства JavaFX и модуля привязки, и не иметь никакой зависимости от других модулей для разработки JavaFX GUI (что может быть полезно для некоторые системы безголовых серверов, которые являются основной целью развертывания для многих приложений Java).

Подобный дизайн изначально был настроен для других средств JavaFX, таких как временные шкалы и переходы, так что система Java в реальном времени с расписанием задач на основе времени по временным шкалам могла использовать модуль анимации/временной шкалы из JavaFX, не завися от остальных. система JavaFX (но я не уверен, что оригинальный дизайн был реализован до сегодняшнего дня, так что это больше не может быть возможным, и базовый импульс модуля анимации обычно привязан к минимальному такту 60 кадров в секунду, возможно, заблокирован при частоте обновления экрана в зависимости от реализации).

Свойства JavaFX не являются общим решением для управления свойствами для Java, но, вероятно, они являются наиболее близкой и наиболее полной реализацией такого решения, которое я видел до сих пор. В идеале (насколько я помню, говорит руководитель проекта JavaFX Ричард Бэйр), функциональность свойств будет встроена в язык программирования Java, поэтому поддержка обеспечивается не только API, но и улучшенным синтаксисом языка. Возможно, какая-то будущая версия Java, такая как 10+, может иметь такие средства, как эти. Конечно, это старая дискуссия, вероятно, восходящая к началу языка Java и спецификаций JDK. Тем не менее, мир не идеален, и механизм свойств JavaFX (даже с таким громоздким синтаксисом и отсутствием встроенной поддержки безопасности потоков) по-прежнему является полезным инструментом для многих приложений. Также обратите внимание, что существуют расширения для других языков, таких как Scala и ScalaFX, которые делают механизм свойств JavaFX частью синтаксиса для этого языка.

Сторонние библиотеки, такие как EasyBind, расширяют механизм свойств JavaFX для лучшей поддержки таких парадигм программирования, как функциональное реактивное программирование.

На данный момент, если бы я хотел широко использовать средства типа свойств в программе JavaFX, я бы, вероятно, основывал его на комбинации свойств JavaFX и (потенциально) EasyBind и ReactFX, так как это, кажется, лучшее решение для Java.

Я бы не стал использовать такое решение в среде с высокой степенью параллелизма, где потоки не разделены из-за отсутствия поддержки безопасности потоков в базовых библиотеках. Свойства основаны на побочных эффектах изменяемых объектов, что довольно сложно рассуждать в многопоточных программах. Даже если внутренняя реализация свойства была изменена, чтобы позволить потокобезопасное чтение/запись свойств, я не уверен, что это был бы такой отличный подход. Для высоко параллельных систем, требующих много одновременных связи между подзадачами, использованием другой парадигмы программирования, такими как актеры (например Akka/Эрлангом) или сообщающимися последовательными процессами может быть более подходящим, чем связанные свойства.

Ответ 2

Я столкнулся с этим сегодня, и было очень полезно, чтобы кто-то указал на потоки. Ошибки, вызванные этим, могут быть коварными.

Я использую это решение широко. Вы не можете связывать Bireirectional с ThreadSafeObjectProperty, но можете привязываться к свойству FX и обновлять его с помощью ThreadSafePropertySetter.

Устроители возвращают будущее. Который может использоваться для управления условиями гонки, вызванными 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)
      }
    }
  }

Ответ 3

Используйте слушателей изменений вместо привязок. Вот хороший метод для этого:

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 но не уточнил, почему. Если бы привязки были безопасны для потока, они все равно сделали бы этот вызов под капотом. Там нет быстрого доступа к безопасности потока, и эти вызовы обслуживаются последовательно, поэтому они безопасны в использовании.