#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;
}
}
}