Использование BackgroundWorker для завершения двух методов один за другим WPF/C#
В моей программе есть два метода, выполнение которых занимает некоторое время, около нескольких минут каждый. Пока эти методы выполняются, я отображаю индикатор выполнения в отдельном окне, в котором отображается прогресс каждого метода. Мои два метода находятся в статическом классе Utility. Они выглядят так:
public static class Utility
{
public static bool TimeConsumingMethodOne(object sender)
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(100);
(sender as BackgroundWorker).ReportProgress(i);
}
return true;
}
public static bool TimeConsumingMethodTwo(object sender)
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
(sender as BackgroundWorker).ReportProgress(i);
}
return true;
}
}
Прочитав аналогичные вопросы в SO, я узнал, что должен использовать BackgroundWorker, и использовал RunWorkerCompleted(), чтобы увидеть, когда работник завершит свою работу. Поэтому в моем Main() я использовал BackgroundWorer() и подписался на метод RunWorkerCompleted(). Моя цель здесь состоит в том, чтобы сначала запустить TimeConsumingMethodOne() (и отобразить прогресс во время работы), затем, когда он закончится, запустить TimeConsumingMethodTwo() и снова показать прогресс, а после этого вывести окно сообщения (которое имитирует некоторые другие операции в моей программе)., Мой Main() выглядит следующим образом:
public partial class MainWindow : Window
{
public enum MethodType
{
One,
Two
}
private BackgroundWorker worker = null;
private AutoResetEvent _resetEventOne = new AutoResetEvent(false);
private AutoResetEvent _resetEventTwo = new AutoResetEvent(false);
private ProgressBarWindow pbWindowOne = null;
private ProgressBarWindow pbWindowTwo = null;
public MainWindow()
{
InitializeComponent();
}
private void btnRun_Click(object sender, RoutedEventArgs e)
{
RunMethodCallers(sender, MethodType.One);
_resetEventOne.WaitOne();
RunMethodCallers(sender, MethodType.Two);
_resetEventTwo.WaitOne();
MessageBox.Show("COMPLETED!");
}
private void RunMethodCallers(object sender, MethodType type)
{
worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
switch (type)
{
case MethodType.One:
worker.DoWork += MethodOneCaller;
worker.ProgressChanged += worker_ProgressChangedOne;
worker.RunWorkerCompleted += worker_RunWorkerCompletedOne;
break;
case MethodType.Two:
worker.DoWork += MethodTwoCaller;
worker.ProgressChanged += worker_ProgressChangedTwo;
worker.RunWorkerCompleted += worker_RunWorkerCompletedTwo;
break;
}
worker.RunWorkerAsync();
}
private void MethodOneCaller(object sender, DoWorkEventArgs e)
{
Dispatcher.Invoke(() =>
{
pbWindowOne = new ProgressBarWindow("Running Method One");
pbWindowOne.Owner = this;
pbWindowOne.Show();
});
Utility.TimeConsumingMethodOne(sender);
}
private void MethodTwoCaller(object sender, DoWorkEventArgs e)
{
Dispatcher.Invoke(() =>
{
pbWindowTwo = new ProgressBarWindow("Running Method Two");
pbWindowTwo.Owner = this;
pbWindowTwo.Show();
});
Utility.TimeConsumingMethodTwo(sender);
}
private void worker_RunWorkerCompletedOne(object sender, RunWorkerCompletedEventArgs e)
{
_resetEventOne.Set();
}
private void worker_RunWorkerCompletedTwo(object sender, RunWorkerCompletedEventArgs e)
{
_resetEventTwo.Set();
}
private void worker_ProgressChangedOne(object sender, ProgressChangedEventArgs e)
{
pbWindowOne.SetProgressUpdate(e.ProgressPercentage);
}
private void worker_ProgressChangedTwo(object sender, ProgressChangedEventArgs e)
{
pbWindowTwo.SetProgressUpdate(e.ProgressPercentage);
}
}
Теперь у меня проблема, когда я использую _resetEventOne.WaitOne(); пользовательский интерфейс висит. Если я удалил эти два ожидания, оба метода запускаются асинхронно, и выполнение продолжается и выводит MessageBox даже до завершения этих двух методов.
Что я делаю неправильно? Как заставить программу завершить мой первый BackgroundWorker, а затем перейти к следующему, а затем, когда это будет сделано, вывести MessageBox?
2 ответа
Теперь у меня проблема, когда я использую _resetEventOne.WaitOne(); пользовательский интерфейс висит. Если я удалил эти два ожидания, оба метода запускаются асинхронно, и выполнение продолжается и выводит MessageBox даже до завершения этих двух методов.
Что я делаю неправильно?
Когда вы звоните WaitOne()
, вы блокируете поток пользовательского интерфейса, в результате чего пользовательский интерфейс зависает. Если вы удалите этот вызов, то, конечно, вы начнете оба работника одновременно.
Есть несколько разных способов подойти к вашему вопросу. Один из них - придерживаться своей текущей реализации и просто установить минимальный минимум, чтобы заставить его работать. Для этого вам нужно выполнить следующее действительное утверждение в RunWorkerCompleted
обработчик, вместо использования события для ожидания выполнения обработчика.
Это выглядит так:
public partial class MainWindow : Window
{
public enum MethodType
{
One,
Two
}
private BackgroundWorker worker = null;
private ProgressBarWindow pbWindowOne = null;
private ProgressBarWindow pbWindowTwo = null;
public MainWindow()
{
InitializeComponent();
}
private void btnRun_Click(object sender, RoutedEventArgs e)
{
RunMethodCallers(sender, MethodType.One);
}
private void RunMethodCallers(object sender, MethodType type)
{
worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
switch (type)
{
case MethodType.One:
worker.DoWork += MethodOneCaller;
worker.ProgressChanged += worker_ProgressChangedOne;
worker.RunWorkerCompleted += worker_RunWorkerCompletedOne;
break;
case MethodType.Two:
worker.DoWork += MethodTwoCaller;
worker.ProgressChanged += worker_ProgressChangedTwo;
worker.RunWorkerCompleted += worker_RunWorkerCompletedTwo;
break;
}
worker.RunWorkerAsync();
}
private void MethodOneCaller(object sender, DoWorkEventArgs e)
{
Dispatcher.Invoke(() =>
{
pbWindowOne = new ProgressBarWindow("Running Method One");
pbWindowOne.Owner = this;
pbWindowOne.Show();
});
Utility.TimeConsumingMethodOne(sender);
}
private void MethodTwoCaller(object sender, DoWorkEventArgs e)
{
Dispatcher.Invoke(() =>
{
pbWindowTwo = new ProgressBarWindow("Running Method Two");
pbWindowTwo.Owner = this;
pbWindowTwo.Show();
});
Utility.TimeConsumingMethodTwo(sender);
}
private void worker_RunWorkerCompletedOne(object sender, RunWorkerCompletedEventArgs e)
{
RunMethodCallers(sender, MethodType.Two);
}
private void worker_RunWorkerCompletedTwo(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("COMPLETED!");
}
private void worker_ProgressChangedOne(object sender, ProgressChangedEventArgs e)
{
pbWindowOne.SetProgressUpdate(e.ProgressPercentage);
}
private void worker_ProgressChangedTwo(object sender, ProgressChangedEventArgs e)
{
pbWindowTwo.SetProgressUpdate(e.ProgressPercentage);
}
}
Это сказало, BackgroundWorker
был сделан устаревшим новым основанным на задачах API с async
а также await
, С некоторыми небольшими изменениями в вашем коде, он может быть адаптирован для использования более новой идиомы:
public partial class MainWindow : Window
{
public enum MethodType
{
One,
Two
}
private ProgressBarWindow pbWindowOne = null;
private ProgressBarWindow pbWindowTwo = null;
public MainWindow()
{
InitializeComponent();
}
private async void btnRun_Click(object sender, RoutedEventArgs e)
{
await RunMethodCallers(sender, MethodType.One);
await RunMethodCallers(sender, MethodType.Two);
MessageBox.Show("COMPLETED!");
}
private async Task RunMethodCallers(object sender, MethodType type)
{
IProgress<int> progress;
switch (type)
{
case MethodType.One:
progress = new Progress<int>(i => pbWindowOne.SetProgressUpdate(i));
await Task.Run(() => MethodOneCaller(progress));
break;
case MethodType.Two:
progress = new Progress<int>(i => pbWindowTwo.SetProgressUpdate(i));
await Task.Run(() => MethodTwoCaller(progress));
break;
}
}
private void MethodOneCaller(IProgress<int> progress)
{
Dispatcher.Invoke(() =>
{
pbWindowOne = new ProgressBarWindow("Running Method One");
pbWindowOne.Owner = this;
pbWindowOne.Show();
});
Utility.TimeConsumingMethodOne(progress);
}
private void MethodTwoCaller(IProgress<int> progress)
{
Dispatcher.Invoke(() =>
{
pbWindowTwo = new ProgressBarWindow("Running Method Two");
pbWindowTwo.Owner = this;
pbWindowTwo.Show();
});
Utility.TimeConsumingMethodTwo(progress);
}
}
Для выполнения вышесказанного требуется небольшая корректировка Utility
класс также:
static class Utility
{
public static bool TimeConsumingMethodOne(IProgress<int> progress)
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(100);
progress.Report(i);
}
return true;
}
public static bool TimeConsumingMethodTwo(IProgress<int> progress)
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
progress.Report(i);
}
return true;
}
}
Это Progress<T>
класс занимает место BackgroundWorker.ProgressChanged
событие и ReportProgress()
метод.
Обратите внимание, что с учетом вышеизложенного код стал значительно короче, проще и написан более прямым способом (т.е. связанные операторы теперь находятся в одном методе).
Приведенный вами пример обязательно упрощен. Это прекрасно, но это означает, что здесь не известно, что Thread.Sleep()
метод представляет. Фактически, во многих случаях такого рода вещи могут подвергаться дальнейшему рефакторингу, так что только длительная работа выполняется асинхронно. Иногда это может упростить отчетность о прогрессе еще больше, потому что это может быть сделано после await
- каждый отдельный асинхронно выполняемый рабочий компонент.
Например, предположим, что работа в цикле либо асинхронна, либо является достаточно дорогостоящей, чтобы ее можно было использовать разумно. Task.Run()
выполнить каждую итерацию цикла. Для того же, что может быть представлено с помощью Task.Delay()
:
static class Utility
{
public static async Task<bool> TimeConsumingMethodOne(Action<int> progress)
{
for (int i = 1; i <= 100; i++)
{
await Task.Delay(100);
progress(i);
}
return true;
}
public static async Task<bool> TimeConsumingMethodTwo(Action<int> progress)
{
for (int i = 1; i <= 100; i++)
{
await Task.Delay(50);
progress(i);
}
return true;
}
}
Выше я тоже не пользуюсь Progress<T>
, Просто простой Action<int>
делегат для вызывающего абонента использовать, как они хотят.
И с этим изменением ваш код окна становится еще проще:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private async void btnRun_Click(object sender, RoutedEventArgs e)
{
await MethodOneCaller();
await MethodTwoCaller();
MessageBox.Show("COMPLETED!");
}
private async Task MethodOneCaller()
{
ProgressBarWindow pbWindowOne =
new ProgressBarWindow("Running Method One") { Owner = this };
pbWindowOne.Show();
await Utility.TimeConsumingMethodOne(i => pbWindowOne.SetProgressUpdate(i));
}
private async Task MethodTwoCaller()
{
ProgressBarWindow pbWindowTwo =
new ProgressBarWindow("Running Method Two") { Owner = this };
pbWindowTwo.Show();
await Utility.TimeConsumingMethodTwo(i => pbWindowTwo.SetProgressUpdate(i));
}
}
Конечно, я воспользовался возможностью, чтобы удалить MethodType
enum и просто вызывайте методы напрямую, что еще больше сокращает код. Но даже если все, что вы сделали, это избежать использования Dispatcher.Invoke()
это все еще сильно упрощает код.
В дополнение ко всему, если вы используете привязку данных для представления состояния прогресса вместо прямой установки значения, WPF будет неявно обрабатывать межпотоковый вызов, так что Progress<T>
класс даже не требуется, даже если вы не можете рефакторинг Utility
код класса для него самого async
,
Но это незначительные улучшения по сравнению с уходом от BackgroundWorker
, Я рекомендую это сделать, но не важно, потратите ли вы время на дальнейшие усовершенствования.
Вариант, который я предпочитаю, это иметь эти 2 метода в другом потоке и использовать цикл while, чтобы проверить, работает ли еще поток и использует ли он Task.Delay() EG.
private async void BlahBahBlahAsync()
{
Thread testThread = new Thread(delegate () { });
newThread = new Thread(delegate ()
{
Timeconsuming();
});
newThread.Start();
while (testThread.IsAlive)
{
await Task.Delay(50);
}
}
private void Timeconsuming()
{
// stuff that takes a while
}