web-dev-qa-db-fra.com

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

J'ai un formulaire qui génère un BackgroundWorker, qui devrait mettre à jour sa propre zone de texte du formulaire (sur le thread principal), d'où l'appel Invoke((Action) (...));.
Si dans HandleClosingEvent je ne fais que bgWorker.CancelAsync(), alors je reçois ObjectDisposedException lors de l'appel Invoke(...), ce qui est compréhensible. Mais si je reste assis dans HandleClosingEvent et que j'attends que bgWorker soit terminé, alors Invoke (...) ne reviendra jamais, également de manière compréhensible. 

Des idées comment puis-je fermer cette application sans obtenir l'exception, ou l'impasse?

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

    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.ToString(); }));
        }
    }

    private void HandleClosingEvent(object sender, CancelEventArgs e) {
        this.bgWorker.CancelAsync();
        /////// while (this.bgWorker.CancellationPending) {} // deadlock
    }
65
THX-1138

Le seul moyen que je connaisse de faire cela dans une impasse et dans une exception est d'annuler l'événement FormClosing. Définissez e.Cancel = true si BGW est toujours en cours d'exécution et définissez un indicateur pour indiquer que l'utilisateur a demandé une fermeture. Vérifiez ensuite cet indicateur dans le gestionnaire d'événements RunWorkerCompleted de 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...
}
94
Hans Passant

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

List<Thread> bgWorkersThreads  = new List<Thread>();

et dans chaque méthode DoWork de backgroundWorker, on obtient:

bgWorkesThreads.Add(Thread.CurrentThread);

Arter que vous pouvez utiliser:

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

Je l'ai utilisé dans Word Add-in dans Control, que j'utilise dans CustomTaskPane. Si quelqu'un ferme le document ou l'application plus tôt, alors tout mon travail backgroundWorkes termine son travail, cela génère un COM Exception (je ne me souviens pas exactement lequel) .CancelAsync() ne fonctionne pas.

Mais avec cela, je peux fermer toutes les discussions qui sont utilisées par backgroundworkers Immédiatement dans l'événement DocumentBeforeClose et mon problème est résolu.

3
Bronix

Voici ma solution (désolé c'est en 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 l'occasion de vérifier la valeur CancellationPending pour définir e.Cancel sur true (ce qui, pour autant que je sache, ne peut être effectué que dans BackgroundWorker1_DoWork) . Je n'ai pas supprimé cette ligne, bien que cela ne semble pas vraiment faire une 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 e.Cancelled ainsi que la variable globale, bClosingForm, avant d'exécuter les étapes finales. 

En utilisant ce modèle, vous devriez pouvoir fermer votre formulaire à tout moment, même si l’agent de travail en arrière-plan est au milieu de quelque chose (ce qui peut ne pas être bon, mais il est inévitable que cela se produise et qu’il soit donc possible de régler le problème). Je ne sais pas si cela est nécessaire, mais vous pouvez supprimer l'agent de travail de fond entièrement dans l'événement Form_Closed une fois que tout ceci s'est produit.

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
2
Tim Hagel

Ne pouvez-vous pas attendre le signal dans le destructeur de la forme? 

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.ToString(); }));
    }
}


private ~Form1()
{
    workerDone.WaitOne();
}


void backgroundWorker1_RunWorkerCompleted( Object sender, RunWorkerCompletedEventArgs e )
{
    workerDone.Set();
}
1
Cheeso

Premièrement, l'exception 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 à plusieurs reprises:

Invoke ou BeginInvoke ne peuvent pas être appelés sur une commande jusqu'à ce que la poignée de fenêtre a été créé.

Je suppose que cela pourrait être modifié en démarrant le travailleur sur le rappel 'Chargé' plutôt que le constructeur, mais toute cette épreuve peut être totalement évitée si le mécanisme de génération de rapports de BackgroundWorker 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.ToString();
}

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 sommeil ci-dessus obstrue l'interface utilisateur, consomme un processeur élevé et augmente continuellement l'utilisation de la mémoire. Je suppose que cela a quelque chose à voir avec la file de messages de la GUI surchargée. Cependant, avec l'appel en veille intact, l'utilisation du processeur est pratiquement égale à 0 et l'utilisation de la mémoire semble correcte également. Pour être prudent, peut-être une valeur supérieure à 1 ms devrait ê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, elle devrait être OK: Lien

Dans tous les cas, je ne peux pas prévoir de scénario dans lequel la mise à jour de l'interface graphique doit être effectuée à des intervalles inférieurs à quelques millisecondes (au moins, dans les scénarios dans lesquels un utilisateur surveille l'interface graphique). le compte rendu des progrès serait le bon choix

1
Ohad Schneider

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

Au cours de l'événement Closed (ou peut-être Closing), le thread d'interface utilisateur se souvient que le formulaire est fermé avant d'annuler le travail en arrière-plan.

À la réception de ProgressChanged, le thread d'interface utilisateur vérifie si le formulaire est fermé et, dans le cas contraire, met à jour la zone de texte.

0
Harald Coppoolse

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

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

}
0
Peter Jamsmenson

Cela ne fonctionnera pas pour tout le monde, mais si vous faites quelque chose dans BackgroundWorker périodiquement, comme toutes les secondes ou toutes les 10 secondes (par exemple, interroger un serveur), cela semble bien fonctionner pour arrêter le processus de manière ordonnée et sans message 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);
            }
    }
0
SnowMan50