3.5.8.1. Примеры использования фоновых задач
Отображение выполнения и управление фоновой задачей с помощью BackgroundWorkProgressWindow

Часто при запуске фоновых задач появляется необходимость отображения простого UI:

  1. показать пользователю, что запрошенное действие находится в процессе выполнения,

  2. дать пользователю возможность прервать запрошенное долгое действие,

  3. показать процент выполнения, если его можно определить.

Для реализации этих потребностей платформа предоставляет вспомогательные классы BackgroundWorkWindow и BackgroundWorkProgressWindow. Эти классы содержат статические методы, позволяющие связать фоновую задачу с модальным диалогом, отображающим заголовок, описание, индикатор прогресса и возможно кнопку Отмена. Разница между этими двумя классами в том, что BackgroundWorkProgressWindow использует определённый индикатор прогресса, и оно должно использоваться только если задача может оценить процент своего выполнения. Класс BackgroundWorkWindow следует использовать для задач, где нельзя оценить прогресс выполнения.

В качестве примера рассмотрим следующую задачу по разработке:

  • Некоторый экран содержит таблицу, отображающую список студентов, с включенным множественным выделением.

  • По нажатию кнопки система должна послать письма-напоминания выбранным студентам, без блокировки UI и с возможностью прервать действие.

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 - обновить индикатор прогресса после каждого посланного письма
Периодическое фоновое обновление данных экрана с использованием Timer и BackgroundTaskWrapper

BackgroundTaskWrapper - это вспомогательный класс, тонкая обертка вокруг BackgroundWorker. Он предоставляет простое API для случаев, когда фоновые задачи одного и того же вида запускаются, перезапускаются и отменяются много раз.

В качестве примера использования рассмотрим следующую задачу по разработке:

  • Имеется экран мониторинга очередей, в котором нужно отображать и автоматически обновлять какие-то табличные данные.

  • Данные загружаются медленно, и поэтому их нужно загружать в фоне.

  • Нужно отображать на экране время последнего обновления.

  • Данные ограничены простым фильтром (флажок checkbox).

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 - проинформировать пользователя, показав уведомление, если загрузка данных завершилась исключением