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