我有一个TableView使用 aRowFactory根据行项目的特定属性来设置行样式。使用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);
}
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
您的代码中有几个误解。第一个是关于单元格和重用(aTableRow是 a Cell)。单元格可以任意重复使用,并且可能频繁地(特别是在用户滚动期间)重复使用以停止显示一项并显示另一项。
在您的代码中,如果该行用于显示无效的实体,则该行上的侦听器itemProperty将触发后台线程上的可运行对象,这将在某个时刻将伪类状态设置为true。
但是,如果随后重用该单元来显示有效项目,则执行的下一个可运行对象不会更改伪类状态。因此该状态保持为 true 并且行颜色保持为红色。
因此,如果某行在某个时刻显示过无效项目,则该行会显示为红色。(如果当前显示无效项目,则不会。)如果滚动足够多,最终所有单元格都会变成红色。
其次,您不得从 FX 应用程序线程以外的任何线程更新属于场景图一部分的任何 UI。此外,一些其他操作,例如创建Window实例(Tooltip是 的子类Window)必须在 FX 应用程序线程上执行。请注意,这包括修改绑定到 UI 的模型属性,包括表列中使用的属性。您在 中违反了这一点validationThread,您在其中创建了一个Tooltip,将其设置在行上,并更改了伪类状态,所有这些都在后台线程中进行。
一个好的方法是使用JavaFX 并发 API。使用Task尽可能仅使用不可变数据并返回不可变值的 s。如果您确实需要更新 UI 中显示的属性,请用于Platform.runLater(...)在 FX 应用程序线程上安排这些更新。
就 MVC 设计而言,模型类存储视图所需的所有数据是一个很好的做法。您的设计遇到了麻烦,因为没有存储验证状态的实际位置。而且,验证状态实际上不仅仅是“有效”或“无效”;它还包括“有效”或“无效”。当线程正在运行但未完成时,有一个阶段验证状态未知。
这是我的解决方案,它解决了这些问题。我假设:
我创建了一个枚举ValidationStatus,它有四个值:
public enum ValidationStatus {
VALID, INVALID, UNKNOWN, PENDING ;
}
Run Code Online (Sandbox Code Playgroud)
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);
}
}
Run Code Online (Sandbox Code Playgroud)
它ValidationService提供了一项服务来验证后台线程上的实体,并使用结果更新相应的属性。这是通过线程池和 JavaFX 进行管理的Task。这只是通过休眠随机时间然后返回交替结果来模拟数据库调用。
当任务更改状态时(即随着其生命周期的进展),实体的验证属性将更新: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 ;
}
}
Run Code Online (Sandbox Code Playgroud)
这就是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);
}
}
Run Code Online (Sandbox Code Playgroud)
这是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);
}
}
Run Code Online (Sandbox Code Playgroud)
这是应用程序类。我添加了编辑名字的功能,这使您可以看到项目恢复为未知,然后重新建立其有效性(您需要在提交编辑后快速更改选择)。
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();
}
}
Run Code Online (Sandbox Code Playgroud)
最后,为了完整性,样式表:
.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);
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
105 次 |
| 最近记录: |