Использование BackgroundWorker для завершения двух методов один за другим WPF/C#


В моей программе есть два метода, выполнение которых занимает некоторое время, около нескольких минут каждый. В то время как эти методы выполняются, я показываю индикатор выполнения в отдельном окне, которое показывает прогресс каждого метода. Мои два метода находятся в статическом служебном классе. Они выглядят следующим образом:

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 (), чтобы увидеть, когда работник завершает свою работу. Так что в моем основном () я используется BackgroundWorer () и подписывается на метод RunWorkerCompleted (). Моя цель здесь состоит в том, чтобы сначала запустить Timeconsumingmethod () (и отображать прогресс во время выполнения), затем, как только закончите, запустите Timeconsumingmethod() и снова покажите прогресс, и когда это будет завершено, выведите окно сообщения (которое имитирует некоторую другую работу в моей программе). Мой 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 2

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 и просто вызвать методы напрямую, что еще больше сократило код. Но даже если все, что вы сделали, это избежали использования Dispatcher.Invoke(), что все еще сильно упрощает код.

В дополнение ко всему этому, если бы вы использовали привязку данных для представления состояния выполнения, а не устанавливали значение напрямую, WPF обработал бы вызов перекрестного потока неявно для вас, так что класс Progress<T> даже не требуется, даже если вы не можете рефакторировать код класса Utility, чтобы он сам был async.

Но это незначительные уточнения по сравнению с удалением от BackgroundWorker. Я рекомендую делать это, но вкладываете ли вы время в эти дальнейшие усовершенствования, менее важно.

Вариант, который я предпочитаю, - это иметь эти 2 метода в другом потоке и использовать цикл while, чтобы проверить, работает ли поток по-прежнему, и если это задача use.Задержка() НАПР..

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
    }