Utilisation de BackgroundWorker pour compléter deux méthodes l’une après l’autre WPF / C #

Dans mon programme, j’ai deux méthodes qui prennent un certain temps, environ quelques minutes chacune. Pendant l’exécution de ces méthodes, j’affiche une barre de progression dans une fenêtre distincte indiquant la progression de chaque méthode. Mes deux méthodes sont dans une classe Utility statique. Ils ressemblent à ceci:

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

En lisant des questions similaires dans SO, j’ai appris que je devais utiliser BackgroundWorker et j’ai utilisé la méthode RunWorkerCompleted () pour voir à quel moment le travailleur avait terminé son travail. Donc, dans ma Main (), j’ai utilisé BackgroundWorer () et ai souscrit à la méthode RunWorkerCompleted (). Mon but ici est d’exécuter TimeConsumingMethodOne () en premier (et d’afficher la progression tout en s’exécutant), puis une fois l’opération terminée, exécutez TimeConsumingMethodTwo () et affichez à nouveau la progression. Une fois l’opération terminée, affichez la boîte de message (qui simule d’autres tâches dans mon programme). . Ma main () ressemble à ceci:

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

Maintenant, le problème que j’ai, c’est quand j’utilise _resetEventOne.WaitOne (); l’interface utilisateur se bloque. Si j’ai supprimé ces deux attentes, les deux méthodes s’exécutent de manière asynchrone et l’exécution continue et génère le MessageBox avant même que ces deux méthodes soient terminées.

Qu’est-ce que je fais mal? Comment faire en sorte que le programme termine mon premier BackgroundWorker, puis passe au suivant, puis lorsque c’est terminé, génère la MessageBox?

    Maintenant, le problème que j’ai, c’est quand j’utilise _resetEventOne.WaitOne (); l’interface utilisateur se bloque. Si j’ai supprimé ces deux attentes, les deux méthodes s’exécutent de manière asynchrone et l’exécution continue et génère le MessageBox avant même que ces deux méthodes soient terminées.

    Qu’est-ce que je fais mal?

    Lorsque vous appelez WaitOne() , vous WaitOne() le thread d’interface utilisateur, ce qui provoque le blocage de l’interface utilisateur. Si vous supprimez cet appel, vous démarrez bien sûr les deux travailleurs en même temps.

    Il y a plusieurs façons différentes d’aborder votre question. L’une consiste à respecter au plus près votre implémentation actuelle et à fixer le minimum nécessaire à son bon fonctionnement. Pour ce faire, vous devez exécuter la prochaine instruction dans le gestionnaire RunWorkerCompleted , au lieu d’utiliser un événement pour attendre l’exécution du gestionnaire.

    Cela ressemble à ceci:

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

    Cela dit, BackgroundWorker a été rendu obsolète par la nouvelle API basée sur les tâches avec async et await . Avec quelques petites modifications à votre code, il peut être adapté pour utiliser ce nouvel idiome:

     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 progress; switch (type) { case MethodType.One: progress = new Progress(i => pbWindowOne.SetProgressUpdate(i)); await Task.Run(() => MethodOneCaller(progress)); break; case MethodType.Two: progress = new Progress(i => pbWindowTwo.SetProgressUpdate(i)); await Task.Run(() => MethodTwoCaller(progress)); break; } } private void MethodOneCaller(IProgress progress) { Dispatcher.Invoke(() => { pbWindowOne = new ProgressBarWindow("Running Method One"); pbWindowOne.Owner = this; pbWindowOne.Show(); }); Utility.TimeConsumingMethodOne(progress); } private void MethodTwoCaller(IProgress progress) { Dispatcher.Invoke(() => { pbWindowTwo = new ProgressBarWindow("Running Method Two"); pbWindowTwo.Owner = this; pbWindowTwo.Show(); }); Utility.TimeConsumingMethodTwo(progress); } } 

    Faire ce qui précède nécessite également un léger ajustement de la classe Utility :

     static class Utility { public static bool TimeConsumingMethodOne(IProgress progress) { for (int i = 1; i <= 100; i++) { Thread.Sleep(100); progress.Report(i); } return true; } public static bool TimeConsumingMethodTwo(IProgress progress) { for (int i = 1; i <= 100; i++) { Thread.Sleep(50); progress.Report(i); } return true; } } 

    En d’autres termes, la classe Progress remplace l’événement BackgroundWorker.ProgressChanged et la méthode ReportProgress() .

    Notez qu'avec ce qui précède, le code est devenu beaucoup plus court, plus simple et écrit de manière plus directe (c'est-à-dire que les instructions associées sont entre elles dans la même méthode).

    L'exemple que vous avez donné est nécessairement simplifié. C'est parfait, mais cela signifie que nous ne Thread.Sleep() pas ici ce que la méthode Thread.Sleep() représente. En fait, dans de nombreux cas, ce genre de chose peut être reformaté de manière à ce que seul le travail de longue durée soit effectué de manière asynchrone. Cela peut parfois simplifier encore davantage les rapports d'avancement, car cela peut être fait après avoir attendu chaque composant de travail exécuté de manière asynchrone.

    Par exemple, supposons que le travail dans la boucle soit insortingnsèquement asynchrone ou soit suffisamment coûteux pour qu'il soit raisonnable d'utiliser Task.Run() pour exécuter chaque itération de boucle. Pour les mêmes Task.Delay() , cela peut être représenté en utilisant Task.Delay() :

     static class Utility { public static async Task TimeConsumingMethodOne(Action progress) { for (int i = 1; i <= 100; i++) { await Task.Delay(100); progress(i); } return true; } public static async Task TimeConsumingMethodTwo(Action progress) { for (int i = 1; i <= 100; i++) { await Task.Delay(50); progress(i); } return true; } } 

    Dans ce qui précède, je n’utilise pas non plus Progress . Il suffit d’un simple délégué Action pour que l’appelant l’utilise à sa guise.

    Et avec ce changement, votre code de fenêtre devient encore plus simple:

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

    Certes, j'en ai profité pour supprimer l'énumération MethodType et simplement appeler les méthodes directement, ce qui a encore raccourci le code. Mais même si vous aviez simplement évité d'utiliser Dispatcher.Invoke() , cela simplifiait encore beaucoup le code.

    De plus, si vous utilisiez la liaison de données pour représenter l'état de progression au lieu de définir directement la valeur, WPF gérerait implicitement l'appel inter-thread, de sorte que la classe Progress ne soit même pas requirejse même si vous ne pouvez pas refactoriser le code de la classe Utility pour qu'il soit async .

    Mais, ce sont des améliorations mineures par rapport à l'abandon de BackgroundWorker . Je recommande de le faire, mais il est moins important d'investir du temps dans ces améliorations.

    Une option que je préfère consiste à avoir ces 2 méthodes dans un thread différent et à utiliser une boucle while pour vérifier si le thread est toujours en cours d’exécution et s’il utilise 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 }