3.5.8.1. 后台任务使用示例
使用 BackgroundWorkProgressWindow 展示和控制后台任务

启动后台任务时,我们一般会要显示一个简单的 UI 界面:

  1. 展示给用户,请求的任务还在执行中,

  2. 允许用户退出长时间执行的任务,

  3. 如果能获取任务执行进度的话,展示目前的进度。

平台通过 BackgroundWorkWindowBackgroundWorkProgressWindow 工具类满足了这些需求。 这些类带有静态方法,可以用来将后台任务和一个模态窗相关联,这个模态窗带有标题、描述、进度条以及一个可选的 Cancel 按钮。 这两个类的区别在于,BackgroundWorkProgressWindow 使用了一个确定的进度条,应当在能估算任务进度的情况下使用。相反,BackgroundWorkWindow 应当在无法估算任务时长的情况使用。

下面我们用一个开发任务作为示例:

  • 一个给定的界面包含展示学生列表的表格,可以多选。

  • 当用户按下某个按钮时,系统会给这些选中的学生发送邮件,而且此时 UI 不会被 block 住,并能取消发送邮件的操作。

bg task emails

示例实现:

import com.haulmont.cuba.gui.backgroundwork.BackgroundWorkProgressWindow;

public class StudentBrowse extends StandardLookup<Student> {

    @Inject
    private Table<Student> studentsTable;

    @Inject
    private EmailService emailService;

    @Subscribe("studentsTable.sendEmail")
    public void onStudentsTableSendEmail(Action.ActionPerformedEvent event) {
        Set<Student> selected = studentsTable.getSelected();
        if (selected.isEmpty()) {
            return;
        }
        BackgroundTask<Integer, Void> task = new EmailTask(selected);
        BackgroundWorkProgressWindow.show(task, (1)
                "Sending reminder emails", "Please wait while emails are being sent",
                selected.size(), true, true (2)
        );
    }

    private class EmailTask extends BackgroundTask<Integer, Void> { (3)
        private Set<Student> students; (4)

        public EmailTask(Set<Student> students) {
            super(10, TimeUnit.MINUTES, StudentBrowse.this); (5)
            this.students = students;
        }

        @Override
        public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
            int i = 0;
            for (Student student : students) {
                if (taskLifeCycle.isCancelled()) { (6)
                    break;
                }
                emailService.sendEmail(student.getEmail(), "Reminder", "Don't forget, the exam is tomorrow",
                        EmailInfo.TEXT_CONTENT_TYPE);

                i++;
                taskLifeCycle.publish(i); (7)
            }
            return null;
        }
    }
}
1 - 启动任务并显示模态进度窗口
2 - 设置对话框选项:进度条的总数、用户可以取消任务、展示进度百分比
3 - 任务进度单位是 Integer(已处理的表格项),结果类型是 Void,因为该任务不会产生结果
4 - 选中的表格项保存在一个变量中,变量在任务的构造器初始化。这是必须要的,因为 run() 方法会在一个后台进程中执行并且没法访问 UI 组件
5 - 设置超时时限为 10 分钟
6 - 周期性的检查 isCancelled(),这样用户按下 Cancel 按钮时能立即结束任务
7 - 每封邮件发出后更新进度条的位置
周期性的在后台使用定时器和 BackgroundTaskWrapper 刷新界面数据

BackgroundTaskWrapper 是一个 BackgroundWorker 的很小的工具包装类。 提供了简单的 API 用来重复的启动、重启和取消同类型的后台任务。

下面这个开发任务示例展示了使用方法:

  • 一个排名监控界面需要展示并自动更新数据。

  • 数据加载很慢,所以需要在后台加载。

  • 在界面展示最新的数据更新时间。

  • 数据通过简单的过滤器(复选框)进行过滤。

bg ranks ok
  • 由于某些原因,如果数据刷新失败了,界面应当告诉用户:

bg ranks error

示例实现:

@UiController("playground_RankMonitor")
@UiDescriptor("rank-monitor.xml")
public class RankMonitor extends Screen {
    @Inject
    private Notifications notifications;
    @Inject
    private Label<String> refreshTimeLabel;
    @Inject
    private CollectionContainer<Rank> ranksDc;
    @Inject
    private RankService rankService;
    @Inject
    private CheckBox onlyActiveBox;
    @Inject
    private Logger log;
    @Inject
    private TimeSource timeSource;
    @Inject
    private Timer refreshTimer;

    private BackgroundTaskWrapper<Void, List<Rank>> refreshTaskWrapper = new BackgroundTaskWrapper<>(); (1)

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        refreshTimer.setDelay(5000);
        refreshTimer.setRepeating(true);
        refreshTimer.start();
    }

    @Subscribe("onlyActiveBox")
    public void onOnlyActiveBoxValueChange(HasValue.ValueChangeEvent<Boolean> event) {
        refreshTaskWrapper.restart(new RefreshScreenTask()); (2)
    }

    @Subscribe("refreshTimer")
    public void onRefreshTimerTimerAction(Timer.TimerActionEvent event) {
        refreshTaskWrapper.restart(new RefreshScreenTask()); (3)
    }

    public class RefreshScreenTask extends BackgroundTask<Void, List<Rank>> { (4)
        private boolean onlyActive; (5)
        protected RefreshScreenTask() {
            super(30, TimeUnit.SECONDS, RankMonitor.this);
            onlyActive = onlyActiveBox.getValue();
        }

        @Override
        public List<Rank> run(TaskLifeCycle<Void> taskLifeCycle) throws Exception {
            List<Rank> data = rankService.loadActiveRanks(onlyActive); (6)
            return data;
        }

        @Override
        public void done(List<Rank> result) { (7)
            List<Rank> mutableItems = ranksDc.getMutableItems();
            mutableItems.clear();
            mutableItems.addAll(result);

            String hhmmss = new SimpleDateFormat("HH:mm:ss").format(timeSource.currentTimestamp());
            refreshTimeLabel.setValue("Last time refreshed: " + hhmmss);
        }

        @Override
        public boolean handleTimeoutException() { (8)
            displayRefreshProblem();
            return true;
        }

        @Override
        public boolean handleException(Exception ex) { (9)
            log.debug("Auto-refresh error", ex);
            displayRefreshProblem();
            return true;
        }

        private void displayRefreshProblem() {
            if (!refreshTimeLabel.getValue().endsWith("(outdated)")) {
                refreshTimeLabel.setValue(refreshTimeLabel.getValue() + " (outdated)");
            }
            notifications.create(Notifications.NotificationType.TRAY)
                    .withCaption("Problem refreshing data")
                    .withHideDelayMs(10_000)
                    .show();
        }
    }
}
1 - 用无参数构造函数初始化 BackgroundTaskWrapper 实例;每次迭代都会提供一个新的任务实例
2 - 复选框值变化时,立即触发一次后台数据更新
3 - 每次计时器刷新触发后台数据更新
4 - 任务不会发布状态信息,所以状态单元是 Void;任务结果类型为 List<Rank>
5 - 复选框状态保存在一个变量中,变量在任务的构造器初始化。这是必须要的,因为 run() 方法会在一个后台进程中执行并且没法访问 UI 组件
6 - 调用自定义的服务加载数据(这是需要在后台执行的长时间任务)
7 - 将成功获取的结果展示到界面组件
8 - 如果数据加载超时,更新 UI:在界面的一个角落展示通知消息
9 - 用通知消息告知用户数据加载失败