Обновить пользовательский интерфейс во время выполнения задачи.Запустить или TaskFactory.StartNew все еще работает

#c# #.net #wpf #task-parallel-library

#c# #.net #wpf #task-parallel-library

Вопрос:

Итак, я решил переписать свой почтовый клиент в WPF, поскольку я думаю, что пришло время перейти от Windows Forms (мне это все еще нравится), но я столкнулся с небольшой проблемой.

Я использую BackgroundWorker в своем приложении Windows Forms для выполнения каких-либо действий и в начале I worker.ReportProgress(currentProgress); , и это позволяет мне обновлять пользовательский интерфейс по мере выполнения задач в фоновом режиме, что здорово.

Но только сейчас, после запуска нового проекта WPF, я замечаю, что в панели инструментов нет BackgroundWorker (для приложений WPF), поэтому я ищу в Интернете и обнаружил, что у некоторых людей возникают проблемы с обновлением пользовательского интерфейса при использовании BackgroundWorker с WPF. Это заставляет меня думать, что использование BackgroundWorker в приложении WPF немного халтурно, а я этого не хочу.

На той же странице другой пользователь ссылается на эту страницу, предлагая им использовать Task.Run вместо BackgroundWorker в WPF. Просмотрев Task.Run документы, я сразу вижу, как это может быть полезно, однако у меня есть одна проблема. Я не вижу способа «Сообщить о прогрессе» или обновить пользовательский интерфейс по мере выполнения. Все, что я вижу, это как запустить задачу и « await » ее; оставляя мне только один вариант — обновить пользовательский интерфейс после завершения длительной задачи.

Как мы можем обновить пользовательский интерфейс настольного приложения WPF, пока Task.Run / TaskFactory.StartNew все еще работает?

Комментарии:

1. BackgroundWorker Класс является одним из основных способов обновления пользовательского интерфейса в фоновом потоке в WPF. Какие статьи вы видели, которые предполагают, что это хакерство?

2. Спасибо @AndrewStephens — причина, по которой я предположил, что это хакерство, заключалась в том, что 1) по умолчанию его нет в наборе инструментов для приложений WPF, 2) Вам нужно вручную добавить worker. SupportsProgress = true; (из-за отсутствия панели свойств для этого элемента управления, 3) Я вижу, что другие люди говорили, что BackgroundWorker не может обновлять пользовательский интерфейс изнутри событий WorkerCompleted или ProgressChanged без использования делегатов (которые не нужны в WinForms) — это наводит меня на мысль, что, возможно, BGWorker не полностью поддерживается или не предназначен для использования в приложениях WPF.

3. Это класс, а не элемент управления пользовательского интерфейса, поэтому его не будет в наборе инструментов. Обычно вы создаете его экземпляр в своем коде или модели представления, если используете MVVM. Событие ProgressChanged вызывается в потоке пользовательского интерфейса, поэтому делегирование не требуется. Я уверен, что WorkerCompleted тоже работает, но если не просто обернуть код обновления пользовательского интерфейса в Dispatcher.Invoke() вызов. Опять же, вы обнаружите, что это распространенный метод в WPF, особенно при обновлении пользовательского интерфейса из других потоков.

4. Большое вам спасибо @Andrew!

Ответ №1:

Вы можете придерживаться BackroundWorker , если вы того пожелаете. В этом нет ничего по-настоящему хакерского, хотя это очень старая школа. Как говорили другие, если вы не можете найти его в своем наборе инструментов, вы всегда можете объявить и инициализировать его прямо из своего кода (не забудьте using System.ComponentModel; директиву).

У Стивена Клири есть отличная серия постов в блоге о BackgroundWorker vs Task , в которых освещаются различия и ограничения каждого подхода. Это определенно стоит прочитать, если вы в затруднении или просто любопытны.

http://blog.stephencleary.com/2013/05/taskrun-vs-backgroundworker-intro.html

Если вы решите пойти по пути Task async/await , есть пара вещей, конкретно связанных с отчетами о ходе выполнения, которые вы должны иметь в виду.

Как правило, вы должны стремиться к тому, чтобы await Task.Run инкапсулировать минимально возможный объем работы. Затем остальная часть вашего async метода будет выполнена в диспетчере SynchronizationContext (при условии, что он был запущен в потоке диспетчера) и сможет напрямую обновлять пользовательский интерфейс, вот так:

 List<object> items = GetItemsToProcess();
int doneSoFar = 0;

foreach (var item in items)
{
    await Task.Run(() => SomeCpuIntensiveWorkAsync(item));

    doneSoFar  ;

    int progressPercentage = (int)((double)doneSoFar / items.Count * 100);

    // Update the UI.
    this.ProgressBar.Value = progressPercentage;
}
  

Это самый простой способ внедрения отчетов о ходе выполнения в async мире.

Единственный раз, когда я могу представить отчет о прогрессе из тела делегата, которому вы передаете Task.Run , — это когда вы обрабатываете очень большое количество элементов, и обработка каждого элемента занимает очень короткое время (мы говорим о 10000 элементах в секунду в качестве приблизительного ориентира). В таком сценарии создание большого количества чрезвычайно мелкозернистых Task файлов и await их редактирование приведет к значительным накладным расходам. Если это ваш случай, вы можете вернуться к механизму отчетов о ходе выполнения, представленному в .NET 4: Progress<T> / IProgress<T> . Это очень похоже на то, как BackgroundWorker отчеты о ходе выполнения (в том смысле, что он зависит от событий), и это обеспечивает немного большую гибкость с точки зрения принятия решения о том, когда вы вернетесь к отправке обратно в контекст диспетчера.

 public async Task DoWorkAsync()
{
    // Let's assume we're on the UI thread now.
    // Dummy up some items to process.
    List<object> items = GetItemsToProcess();

    // Wire up progress reporting.
    // Creating a new instance of Progress
    // will capture the SynchronizationContext
    // any any calls to IProgress.Report
    // will be posted to that context.
    Progress<int> progress = new Progress<int>();

    progress.ProgressChanged  = (sender, progressPercentage) =>
    {
        // This callback will run on the thread which
        // created the Progress<int> instance.
        // You can update your UI here.
        this.ProgressBar.Value = progressPercentage;
    };

    await Task.Run(() => this.LongRunningCpuBoundOperation(items, progress));
}

private void LongRunningCpuBoundOperation(List<object> items, IProgress<int> progress)
{
    int doneSoFar = 0;
    int lastReportedProgress = -1;

    foreach (var item in items)
    {
        // Process item.
        Thread.Sleep(1);

        // Calculate and report progress.
        doneSoFar  ;

        var progressPercentage = (int)((double)doneSoFar / items.Count * 100);

        // Only post back to the dispatcher SynchronizationContext
        // if the progress percentage actually changed.
        if (progressPercentage != lastReportedProgress)
        {
            // Note that progress is IProgress<int>,
            // not Progress<int>. This is important
            // because Progress<int> implements
            // IProgress<int>.Report explicitly.
            progress.Report(progressPercentage);

            lastReportedProgress = progressPercentage;
        }
    }
}