TableView 滚动和排序会导致 RowFactory 的行样式不正确

Ren*_*235 2 java javafx

我有一个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)

截屏

截屏

Jam*_*s_D 5

您的代码中有几个误解。第一个是关于单元格和重用(aTableRow是 a Cell)。单元格可以任意重复使用,并且可能频繁地(特别是在用户滚动期间)重复使用以停止显示一项并显示另一项。

在您的代码中,如果该行用于显示无效的实体,则该行上的侦听器itemProperty将触发后台线程上的可运行对象,这将在某个时刻将伪类状态设置为true

但是,如果随后重用该单元来显示有效项目,则执行的下一个可运行对象不会更改伪类状态。因此该状态保持为 true 并且行颜色保持为红色。

因此,如果某行在某个时刻显示过无效项目,则该行会显示为红色。(如果当前显示无效项目,则不会。)如果滚动足够多,最终所有单元格都会变成红色。

其次,您不得从 FX 应用程序线程以外的任何线程更新属于场景图一部分的任何 UI。此外,一些其他操作,例如创建Window实例(Tooltip是 的子类Window)必须在 FX 应用程序线程上执行。请注意,这包括修改绑定到 UI 的模型属性,包括表列中使用的属性。您在 中违反了这一点validationThread,您在其中创建了一个Tooltip,将其设置在行上,并更改了伪类状态,所有这些都在后台线程中进行。

一个好的方法是使用JavaFX 并发 API。使用Task尽可能仅使用不可变数据并返回不可变值的 s。如果您确实需要更新 UI 中显示的属性,请用于Platform.runLater(...)在 FX 应用程序线程上安排这些更新。

就 MVC 设计而言,模型类存储视图所需的所有数据是一个很好的做法。您的设计遇到了麻烦,因为没有存储验证状态的实际位置。而且,验证状态实际上不仅仅是“有效”或“无效”;它还包括“有效”或“无效”。当线程正在运行但未完成时,有一个阶段验证状态未知。

这是我的解决方案,它解决了这些问题。我假设:

  1. 您的实体有有效性的概念。
  2. 建立实体的有效性是一个长期的过程
  3. 有效性取决于一个或多个属性,这些属性在 UI 显示时可能会发生变化
  4. 有效性应该根据需要“惰性地”建立。
  5. UI 不希望显示“未知”有效性,如果显示有效性未知的实体,则应建立并重新显示该实体。

我创建了一个枚举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如果任务处于未完成状态,则 或VALIDINVALID具体取决于以下结果任务,如果任务成功。

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)