Comment arrêter l’événement BackgroundWorker on Closing?

J’ai un formulaire qui génère un BackgroundWorker, qui devrait mettre à jour la zone de texte du formulaire (sur le thread principal), donc Invoke((Action) (...)); appel.
Si dans HandleClosingEvent je fais juste bgWorker.CancelAsync() alors j’obtiens ObjectDisposedException sur Invoke(...) , naturellement. Mais si je m’assieds dans HandleClosingEvent et que j’attends que bgWorker soit terminé, alors .Invoke (…) ne revient jamais, aussi compréhensible.

Des idées comment fermer cette application sans obtenir l’exception, ou l’impasse?

Voici 3 méthodes pertinentes de la classe simple Form1:

  public Form1() { InitializeComponent(); Closing += HandleClosingEvent; this.bgWorker.RunWorkerAsync(); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { while (!this.bgWorker.CancellationPending) { Invoke((Action) (() => { this.textBox1.Text = Environment.TickCount.ToSsortingng(); })); } } private void HandleClosingEvent(object sender, CancelEventArgs e) { this.bgWorker.CancelAsync(); /////// while (this.bgWorker.CancellationPending) {} // deadlock } 

Le seul moyen sûr et sûr de le faire, à ma connaissance, est d’annuler l’événement FormClosing. Définissez e.Cancel = true si le BGW est toujours en cours d’exécution et définissez un indicateur pour indiquer que l’utilisateur a demandé une fermeture. Ensuite, vérifiez cet indicateur dans le gestionnaire d’événements RunWorkerCompleted du BGW et appelez Close () s’il est défini.

 private bool closePending; protected override void OnFormClosing(FormClosingEventArgs e) { if (backgroundWorker1.IsBusy) { closePending = true; backgroundWorker1.CancelAsync(); e.Cancel = true; this.Enabled = false; // or this.Hide() return; } base.OnFormClosing(e); } void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (closePending) this.Close(); closePending = false; // etc... } 

J’ai trouvé un autre moyen. Si vous avez plus de backgroundWorkers, vous pouvez faire:

 List bgWorkersThreads = new List(); 

et dans la méthode DoWork de chaque backgroundWorker:

 bgWorkesThreads.Add(Thread.CurrentThread); 

Arter que vous pouvez utiliser:

 foreach (Thread thread in this.bgWorkersThreads) { thread.Abort(); } 

Je l’ai utilisé dans le complément Word dans Control, que j’utilise dans CustomTaskPane . Si quelqu’un ferme le document ou l’application plus tôt que tous mes backgroundWorkes finissent leur travail, cela soulève une COM Exception (je ne me souviens plus exactement quoi). CancelAsync() ne fonctionne pas.

Mais avec ceci, je peux fermer tous les threads qui sont utilisés par les backgroundworkers du backgroundworkers immédiatement dans l’événement DocumentBeforeClose et mon problème est résolu.

Voici ma solution (Désolé, c’est dans VB.Net).

Lorsque j’exécute l’événement FormClosing, j’exécute BackgroundWorker1.CancelAsync () pour définir la valeur CancellationPending sur True. Malheureusement, le programme n’a jamais vraiment la possibilité de vérifier la valeur de la valeur CancellationPending pour définir e.Cancel sur true (ce qui, à ce que je sache, ne peut être fait que dans BackgroundWorker1_DoWork). Je n’ai pas supprimé cette ligne, bien que cela ne semble pas vraiment faire la différence.

J’ai ajouté une ligne qui définirait ma variable globale, bClosingForm, sur True. Ensuite, j’ai ajouté une ligne de code dans BackgroundWorker_WorkCompleted pour vérifier à la fois la variable globale, bClosingForm, et eCancelled, avant d’effectuer les étapes de fin.

En utilisant ce modèle, vous devriez être en mesure de fermer votre formulaire à tout moment même si le travailleur d’arrière-plan est au milieu de quelque chose (ce qui n’est peut-être pas bien, mais cela risque de se produire). Je ne suis pas sûr que ce soit nécessaire, mais vous pouvez éliminer le travailleur en arrière-plan entièrement dans l’événement Form_Closed, une fois que tout cela a eu lieu.

 Private bClosingForm As Boolean = False Private Sub SomeFormName_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing bClosingForm = True BackgroundWorker1.CancelAsync() End Sub Private Sub backgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork 'Run background tasks: If BackgroundWorker1.CancellationPending Then e.Cancel = True Else 'Background work here End If End Sub Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted If Not bClosingForm Then If Not e.Cancelled Then 'Completion Work here End If End If End Sub 

Ne pouvez-vous pas attendre le signal dans le destructeur du formulaire?

 AutoResetEvent workerDone = new AutoResetEvent(); private void HandleClosingEvent(object sender, CancelEventArgs e) { this.bgWorker.CancelAsync(); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { while (!this.bgWorker.CancellationPending) { Invoke((Action) (() => { this.textBox1.Text = Environment.TickCount.ToSsortingng(); })); } } private ~Form1() { workerDone.WaitOne(); } void backgroundWorker1_RunWorkerCompleted( Object sender, RunWorkerCompletedEventArgs e ) { workerDone.Set(); } 

Tout d’abord, l’ObjectDisposedException n’est qu’un piège possible ici. L’exécution du code de l’OP a généré l’exception InvalidOperationException suivante à un nombre considérable d’occasions:

Invoke ou BeginInvoke ne peut pas être appelé sur un contrôle tant que le handle de fenêtre n’a pas été créé.

Je suppose que cela pourrait être modifié en démarrant le travailleur sur le rappel «Loaded» plutôt que sur le constructeur, mais cette épreuve entière peut être évitée si le mécanisme de rapport ProgressWorker Progress est utilisé. Ce qui suit fonctionne bien:

 private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { while (!this.bgWorker.CancellationPending) { this.bgWorker.ReportProgress(Environment.TickCount); Thread.Sleep(1); } } private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.textBox1.Text = e.ProgressPercentage.ToSsortingng(); } 

J’ai en quelque sorte détourné le paramètre de pourcentage mais on peut utiliser l’autre surcharge pour passer n’importe quel paramètre.

Il est intéressant de noter que la suppression de l’appel de veille ci-dessus obstrue l’UI, consum beaucoup de CPU et augmente continuellement l’utilisation de la mémoire. Je suppose que cela a quelque chose à voir avec la queue des messages de l’interface graphique en cours de surcharge. Cependant, avec l’appel de veille intact, l’utilisation du processeur est pratiquement nulle et l’utilisation de la mémoire semble également correcte. Pour être prudent, une valeur supérieure à 1 ms devrait peut-être être utilisée? Un avis d’expert ici serait apprécié … Mise à jour : Il semble que tant que la mise à jour n’est pas trop fréquente, cela devrait être OK: Lien

Dans tous les cas, je ne peux pas prévoir un scénario où la mise à jour de l’interface graphique doit être effectuée à des intervalles inférieurs à quelques millisecondes (du moins, dans des scénarios où une personne regarde l’interface graphique). rapport de progrès serait le bon choix

Votre agent d’arrière-plan ne doit pas utiliser Invoke pour mettre à jour la zone de texte. Il devrait demander au thread d’interface utilisateur de mettre à jour la zone de texte en utilisant l’événement ProgressChanged avec la valeur à mettre dans la zone de texte attachée.

Lors de l’événement Closed (ou peut-être de la clôture de l’événement), le thread d’interface utilisateur se souvient que le formulaire est fermé avant qu’il annule le travailleur en arrière-plan.

Lors de la réception de la progression, le thread UI vérifie si le formulaire est fermé et seulement sinon, il met à jour la zone de texte.

Cela ne fonctionnera pas pour tout le monde, mais si vous faites quelque chose régulièrement dans BackgroundWorker, comme toutes les secondes ou toutes les 10 secondes (par exemple en interrogeant un serveur), cela semble fonctionner correctement pour arrêter le processus de manière ordonnée et sans messages d’erreur (au moins jusqu’à présent) et est facile à suivre;

  public void StopPoll() { MyBackgroundWorker.CancelAsync(); //Cancel background worker AutoResetEvent1.Set(); //Release delay so cancellation occurs soon } private void bw_DoWork(object sender, DoWorkEventArgs e) { while (!MyBackgroundWorker.CancellationPending) { //Do some background stuff MyBackgroundWorker.ReportProgress(0, (object)SomeData); AutoResetEvent1.WaitOne(10000); } } 

Je ne vois vraiment pas pourquoi DoEvents est considéré comme un si mauvais choix dans ce cas si vous utilisez this.enabled = false. Je pense que cela le rendrait très propre.

 protected override void OnFormClosing(FormClosingEventArgs e) { this.Enabled = false; // or this.Hide() e.Cancel = true; backgroundWorker1.CancelAsync(); while (backgroundWorker1.IsBusy) { Application.DoEvents(); } e.cancel = false; base.OnFormClosing(e); } 

Je transmettrais le SynchronizationContext associé à la zone de texte à BackgroundWorker et l’utilisais pour effectuer des mises à jour sur le thread d’interface utilisateur. En utilisant SynchronizationContext.Post, vous pouvez vérifier si le contrôle est éliminé ou éliminé.

Qu’en est-il de moi.IsHandleCreated?

  Private Sub BwDownload_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BwDownload.RunWorkerCompleted If Me.IsHandleCreated Then 'Form is still open, so proceed End If End Sub 

Autrement:

 if (backgroundWorker.IsBusy) { backgroundWorker.CancelAsync(); while (backgroundWorker.IsBusy) { Application.DoEvents(); } } 

Une solution qui fonctionne, mais trop compliquée. L’idée est de générer le minuteur qui continuera à fermer le formulaire, et le formulaire refusera de se fermer jusqu’à ce que le bgWorker soit mort.

 private void HandleClosingEvent(object sender, CancelEventArgs e) { if (!this.bgWorker.IsBusy) { // bgWorker is dead, let Closing event proceed. e.Cancel = false; return; } if (!this.bgWorker.CancellationPending) { // it is first call to Closing, cancel the bgWorker. this.bgWorker.CancelAsync(); this.timer1.Enabled = true; } // either this is first attempt to close the form, or bgWorker isn't dead. e.Cancel = true; } private void timer1_Tick(object sender, EventArgs e) { Trace.WriteLine("Trying to close..."); Close(); }