У меня есть TableView
, который использует RowFactory
для стилизации строк в зависимости от определенного свойства элемента строки. RowFactory
использует рабочие потоки для проверки правильности этого конкретного свойства при обращении к базе данных. Проблема в том, что правильные строки иногда помечаются как неправильные (красный через PseudoClass
), а неправильные строки не помечаются. Я создал минимальный воспроизводимый пример ниже. В этом примере должны быть отмечены только четные строки... но также отмечены и другие строки.
public class TestEntity
{
public TestEntity(String firstName, String lastName, int c)
{
setFirstName(firstName);
setLastName(lastName);
setC(c);
}
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private IntegerProperty c = new SimpleIntegerProperty();
public int getC()
{
return c.get();
}
public IntegerProperty cProperty()
{
return c;
}
public void setC(int c)
{
this.c.set(c);
}
public String getFirstName()
{
return firstName.get();
}
public StringProperty firstNameProperty()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName.set(firstName);
}
public String getLastName()
{
return lastName.get();
}
public StringProperty lastNameProperty()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName.set(lastName);
}
}
public class TableViewProblemMain extends Application
{
public static void main(String[] args)
{
launch(args);
AppThreadPool.shutdown();
}
@Override
public void start(Stage stage)
{
TableView<TestEntity> tableView = new TableView();
TableColumn<TestEntity, String> column1 = new TableColumn<>("First Name");
column1.setCellValueFactory(new PropertyValueFactory<>("firstName"));
TableColumn<TestEntity, String> column2 = new TableColumn<>("Last Name");
column2.setCellValueFactory(new PropertyValueFactory<>("lastName"));
TableColumn<TestEntity, String> column3 = new TableColumn<>("C");
column3.setCellValueFactory(new PropertyValueFactory<>("c"));
tableView.getColumns().addAll(column1, column2, column3);
tableView.setRowFactory(new TestRowFactory());
for (int i = 0; i < 300; i++)
{
tableView.getItems().add(new TestEntity("Fname" + i, "Lname" + i, i));
}
VBox vbox = new VBox(tableView);
Scene scene = new Scene(vbox);
scene.getStylesheets().add(this.getClass().getResource("/style.css").toExternalForm());
// Css has only these lines:
/*
.table-row-cell:invalid {
-fx-background-color: rgba(240, 116, 116, 0.18);
}
* */
stage.setScene(scene);
stage.show();
}
}
public class TestRowFactory implements Callback<TableView<TestEntity>, TableRow<TestEntity>>
{
private final PseudoClass INVALID_PCLASS = PseudoClass.getPseudoClass("invalid");
@Override
public TableRow<TestEntity> call(TableView param)
{
TableRow<TestEntity> row = new TableRow();
Thread validationThread = new Thread(() ->
{
try
{
if (row.getItem() != null)
{
Thread.sleep(500); // perform validation and stuff...
if (row.getItem().getC() % 2 == 0)
{
Tooltip t = new Tooltip("I am a new tooltip that should be shown only on red rows");
row.setTooltip(t);
row.pseudoClassStateChanged(INVALID_PCLASS, true);
}
}
} catch (InterruptedException e)
{
e.printStackTrace();
}
});
ChangeListener changeListener = (obs, old, current) ->
{
row.setTooltip(null);
AppThreadPool.perform(validationThread);
};
row.itemProperty().addListener((observable, oldValue, newValue) ->
{
row.setTooltip(null);
if (oldValue != null)
{
oldValue.firstNameProperty().removeListener(changeListener);
}
if (newValue != null)
{
newValue.firstNameProperty().removeListener(changeListener);
AppThreadPool.perform(validationThread);
}
else
{
row.pseudoClassStateChanged(INVALID_PCLASS, false);
}
});
row.focusedProperty().addListener(changeListener);
return row;
}
}
public class AppThreadPool
{
private static final int threadCount = Runtime.getRuntime().availableProcessors();
private static final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount * 2 + 1);
public static <R extends Runnable> void perform(R runnable)
{
executorService.submit(runnable);
}
public static void shutdown()
{
executorService.shutdown();
}
}
кстати: кажется неправильным позволять строке решать о достоверности (или запускать любую маршрутизацию проверки) - это должно быть выполнено в самой модели/данных, а затем иметь список данных с экстрактором для этого свойства (и, возможно, другие, которые могут потребовать строка для обновления) - при этом не должно быть необходимости в слушателях для элемента
пожалуйста, предоставьте полный класс сущности - вы хотите максимально упростить для потенциальных помощников запуск примера и оказание помощи, не так ли :)
Существует много серьезных недопониманий о том, как все это работает. 1. Как указано в другом комментарии, вы не должен изменяете или получаете доступ к состоянию графа сцены из фонового потока. 2. Ячейки (строки) повторно используются, возможно, очень часто, (преднамеренно) неопределенным образом. Если вы когда-либо устанавливали свойство ячейки на основе каких-то данных, вы всегда должны устанавливать это свойство при обновлении ячейки. 3. Представление (строка таблицы) — совершенно неподходящее место для манипулирования данными. «Действительное» состояние вашей сущности должно где-то управляться в модели.
Итак, это хороший минимальный воспроизводимый пример, но для его исправления требуются некоторые знания о том, что вы на самом деле хотите делать в своем реальном приложении. Мои догадки таковы: 1. У вас есть объект, который может находиться в действительном состоянии, а может и не находиться. 2. Вы хотите указать пользователю любые отображаемые объекты, которые являются недействительными. 3. Установление достоверности — длительный процесс. 4. Действительное состояние данного объекта зависит от некоторого свойства, которое является изменчивым и может изменяться во время работы приложения (я предполагаю, что это основано на вашем слушателе на firstNameProperty
). Если все верно, отредактируйте вопрос, чтобы включить его.
@kleopatra Я старался ничего не менять в своем решении, чтобы каждую ошибку можно было упомянуть в ответе и никогда не забывать :). Использование модели звучит как очень хорошая идея, и я прямо сейчас рассмотрю ответ James_D. Класс сущности обновлен.
В вашем коде есть несколько недоразумений. Первый касается ячеек и повторного использования (TableRow
есть Cell
). Ячейки можно повторно использовать произвольно и потенциально часто (особенно во время прокрутки пользователем), чтобы остановить отображение одного элемента и отобразить другой.
В вашем коде, если строка используется для отображения недопустимой сущности, слушатель в строке itemProperty
вызовет исполняемый объект в фоновом потоке, который в какой-то момент установит состояние псевдокласса в true
.
Однако, если ячейка впоследствии повторно используется для отображения допустимого элемента, следующий исполняемый объект, который выполняется, не меняет состояние псевдокласса. Таким образом, это состояние остается истинным, а цвет строки остается красным.
Следовательно, строка становится красной, если в какой-то момент она когда-либо отображала недопустимый элемент. (Нет, если в настоящее время он отображает недопустимый элемент.) Если вы достаточно прокрутите, в конечном итоге все ячейки станут красными.
Во-вторых, вы не должен обновляете любой пользовательский интерфейс, являющийся частью графа сцены, из любого потока, кроме потока приложения FX. Кроме того, некоторые другие операции, такие как создание экземпляров Window
(Tooltip
является подклассом Window
), должны выполняться в потоке приложения FX. Обратите внимание, что это включает в себя изменение свойств модели, привязанных к пользовательскому интерфейсу, включая свойства, используемые в столбцах таблицы. Вы нарушаете это в своем validationThread
, где вы создаете Tooltip
, устанавливаете его в строке и изменяете состояние псевдокласса, и все это в фоновом потоке.
Здесь хорошим подходом является использование API параллелизма JavaFX. Используйте Task
, которые, насколько это возможно, используют только неизменяемые данные и возвращают неизменяемое значение. Если вам нужно обновить свойства, которые отображаются в пользовательском интерфейсе, используйте Platform.runLater(...)
, чтобы запланировать эти обновления в потоке приложения FX.
С точки зрения дизайна MVC, хорошей практикой для вашего класса (ов) модели является хранение всех данных, необходимых представлению. Ваш дизайн сталкивается с проблемой, потому что нет реального места, где хранится статус проверки. Более того, статус проверки на самом деле больше, чем просто «действительный» или «недействительный»; существует фаза, когда поток выполняется, но не завершен, и статус проверки неизвестен.
Вот мое решение, которое решает эти проблемы. Я предполагаю:
Я создал перечисление для ValidationStatus
, которое имеет четыре значения:
public enum ValidationStatus {
VALID, INVALID, UNKNOWN, PENDING ;
}
UNKNOWN
указывает, что достоверность неизвестна и проверка не запрашивалась; PENDING
указывает, что проверка была запрошена, но еще не завершена.
Затем у меня есть оболочка для вашего объекта, которая добавляет статус проверки в качестве наблюдаемого свойства. Если свойство, от которого зависит проверка в базовой сущности, изменяется, проверка сбрасывается на UNKNOWN
.
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class ValidatingTestEntity {
private final TestEntity entity ;
private final ObjectProperty<ValidationStatus> validationStatus = new SimpleObjectProperty<>(ValidationStatus.UNKNOWN);
public ValidatingTestEntity(TestEntity entity) {
this.entity = entity;
entity.firstNameProperty().addListener((obs, oldName, newName) -> setValidationStatus(ValidationStatus.UNKNOWN));
}
public TestEntity getEntity() {
return entity;
}
public ValidationStatus getValidationStatus() {
return validationStatus.get();
}
public ObjectProperty<ValidationStatus> validationStatusProperty() {
return validationStatus;
}
public void setValidationStatus(ValidationStatus validationStatus) {
this.validationStatus.set(validationStatus);
}
}
ValidationService
предоставляет сервис для проверки объектов в фоновом потоке, обновляя соответствующие свойства с результатом. Это управляется через пул потоков и с помощью JavaFX Task
s. Это просто имитирует вызов базы данных, засыпая в течение случайного промежутка времени, а затем возвращая чередующиеся результаты.
Когда задача меняет состояние (т. е. по мере прохождения своего жизненного цикла), свойство проверки объекта обновляется: UNKNOWN
, если задача не завершается нормально, PENDING
, если задача находится в незавершенном состоянии, и либо VALID
, либо INVALID
, в зависимости от по результату задания, если задание выполнено успешно.
import javafx.application.Platform;
import javafx.concurrent.Task;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
public class ValidationService {
private final Executor exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2 + 1,
r -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
);
public Task<Boolean> validateEntity(ValidatingTestEntity entity) {
// task runs on a background thread and should not access mutable data,
// so make final copies of anything needed here:
final String firstName = entity.getEntity().getFirstName();
final int code =entity.getEntity().getC();
Task<Boolean> task = new Task<Boolean>() {
@Override
protected Boolean call() throws Exception {
try {
Thread.sleep(500 + ThreadLocalRandom.current().nextInt(500));
} catch (InterruptedException exc) {
// if interrupted other than being cancelled, reset thread's interrupt status:
if (! isCancelled()) {
Thread.currentThread().interrupt();
}
}
boolean result = code % 2 == 0;
return result;
}
};
task.stateProperty().addListener((obs, oldState, newState) ->
entity.setValidationStatus(
switch(newState) {
case CANCELLED, FAILED -> ValidationStatus.UNKNOWN;
case READY, RUNNING, SCHEDULED -> ValidationStatus.PENDING ;
case SUCCEEDED ->
task.getValue() ? ValidationStatus.VALID : ValidationStatus.INVALID ;
}
)
);
exec.execute(task);
return task ;
}
}
Это TableRow
реализация. У него есть прослушиватель, который наблюдает за статусом проверки текущего элемента, если он есть. Если элемент изменяется, прослушиватель удаляется из старого элемента (если он есть) и прикрепляется к новому элементу (если он есть). Если изменяется элемент или изменяется состояние проверки текущего элемента, строка обновляется. Если новый статус проверки — UNKNOWN
, в службу отправляется запрос на проверку текущего элемента. Существует два состояния псевдокласса: недействительный (красный) и неизвестный (оранжевый), которые обновляются каждый раз при изменении элемента или его статуса проверки. Всплывающая подсказка устанавливается, если элемент недействителен, в противном случае устанавливается значение null.
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.control.TableRow;
import javafx.scene.control.Tooltip;
public class ValidatingTableRow extends TableRow<ValidatingTestEntity> {
private final ValidationService validationService ;
private final PseudoClass pending = PseudoClass.getPseudoClass("pending");
private final PseudoClass invalid = PseudoClass.getPseudoClass("invalid");
private final Tooltip tooltip = new Tooltip();
private final ChangeListener<ValidationStatus> listener = (obs, oldStatus, newStatus) -> {
updateValidationStatus();
};
public ValidatingTableRow(ValidationService validationService){
this.validationService = validationService ;
itemProperty().addListener((obs, oldItem, newItem) -> {
setTooltip(null);
if (oldItem != null) {
oldItem.validationStatusProperty().removeListener(listener);
}
if (newItem != null) {
newItem.validationStatusProperty().addListener(listener);
}
updateValidationStatus();
});
}
private void updateValidationStatus() {
if (getItem() == null) {
pseudoClassStateChanged(pending, false);
pseudoClassStateChanged(invalid, false);
setTooltip(null);
return ;
}
ValidationStatus validationStatus = getItem().getValidationStatus();
if ( validationStatus == ValidationStatus.UNKNOWN) {
validationService.validateEntity(getItem());
}
if (validationStatus == ValidationStatus.INVALID) {
tooltip.setText("Invalid entity: "+getItem().getEntity().getFirstName() + " " +getItem().getEntity().getC());
setTooltip(tooltip);
} else {
setTooltip(null);
}
pseudoClassStateChanged(pending, validationStatus == ValidationStatus.PENDING);
pseudoClassStateChanged(invalid, validationStatus == ValidationStatus.INVALID);
}
}
Вот Entity
, то же самое, что и в вопросе:
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class TestEntity
{
public TestEntity(String firstName, String lastName, int c)
{
setFirstName(firstName);
setLastName(lastName);
setC(c);
}
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private IntegerProperty c = new SimpleIntegerProperty();
public String getFirstName() {
return firstName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public String getLastName() {
return lastName.get();
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public int getC() {
return c.get();
}
public IntegerProperty cProperty() {
return c;
}
public void setC(int c) {
this.c.set(c);
}
}
А вот и класс приложения. Я добавил возможность редактировать первое имя, что позволяет вам увидеть, как элемент возвращается к неизвестному, а затем восстанавливает свою действительность (вам нужно быстро изменить выбор после фиксации редактирования).
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TableViewProblemMain extends Application
{
public static void main(String[] args)
{
launch(args);
}
@Override
public void start(Stage stage)
{
TableView<ValidatingTestEntity> tableView = new TableView();
tableView.setEditable(true);
TableColumn<ValidatingTestEntity, String> column1 = new TableColumn<>("First Name");
column1.setCellValueFactory(cellData -> cellData.getValue().getEntity().firstNameProperty());
column1.setEditable(true);
column1.setCellFactory(TextFieldTableCell.forTableColumn());
TableColumn<ValidatingTestEntity, String> column2 = new TableColumn<>("Last Name");
column2.setCellValueFactory(cellData -> cellData.getValue().getEntity().lastNameProperty());
TableColumn<ValidatingTestEntity, Number> column3 = new TableColumn<>("C");
column3.setCellValueFactory(cellData -> cellData.getValue().getEntity().cProperty());
tableView.getColumns().addAll(column1, column2, column3);
ValidationService service = new ValidationService();
tableView.setRowFactory(tv -> new ValidatingTableRow(service));
for (int i = 0; i < 300; i++)
{
tableView.getItems().add(new ValidatingTestEntity(
new TestEntity("Fname" + i, "Lname" + i, i)));
}
VBox vbox = new VBox(tableView);
Scene scene = new Scene(vbox);
scene.getStylesheets().add(this.getClass().getResource("/style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
}
Наконец, для полноты, таблица стилей:
.table-row-cell:invalid {
-fx-background-color: rgba(240, 116, 116, 0.18);
}
.table-row-cell:pending {
-fx-background-color: rgba(240, 120, 0, 0.18);
}
Это был один из самых сложных ответов, которые я когда-либо видел на SO. Очень хорошо объяснил, и я понял каждую из своих ошибок / ошибок. Я ценю время, которое вы потратили, чтобы написать все это, и мне жаль, что я не могу дать больше, чем один голос.
хм ... вы знаете, что мне неудобно с таким количеством логики в строке: это просто не его работа, ИМО ;) Наличие слушателей свойств ячейки является хрупким (слишком много может и часто идет не так). Вместо этого я предпочитаю выполнять всю проверку вне представления и позволять ячейке обновлять себя при изменении состояния проверки. Что, по общему признанию, немного затруднено для строк из-за проблем.
@kleopatra Конечно: это компромисс. И вы можете переписать прослушиватель на itemProperty
, переопределив updateItem()
, хотя доступ к «старому элементу» немного менее прозрачен и больше зависит от базовой реализации ячейки. Суть в том, что разные шаблоны MV* лучше работают в разных сценариях. Предполагая, что требование состоит в том, чтобы выполнять дорогостоящую проверку только для отображаемых элементов, это взывает к MVVM, где в виртуальной машине есть свойство displayed
, которое вы можете использовать. Но строки таблицы (и вообще ячейки) просто так не реализованы.
в основном согласен, пробег варьируется - и, возможно, преувеличивает мои личные предпочтения :) Хотя, по моему опыту, проверка должна (почти?) всегда выполняться независимо от показа. Но тогда это может быть всего лишь одно предположение слишком много, и вопрос на самом деле не слишком ясен в контексте.
хорошо привести пример, но повторюсь (комментарий к вашему предыдущему вопросу: stackoverflow.com/questions/71494286/…): вы не должен измените свойства узла в графике активной сцены вне потока приложения fx (как вы делаете, устанавливая всплывающую подсказку и обновляя псевдосостояние изнутри фоновая ветка)