2009-05-04 5 views
3

J'ai un UserControl simple pour la pagination de base de données, qui utilise un contrôleur pour effectuer les appels DAL réels. J'utilise un BackgroundWorker pour effectuer le levage lourd, et sur l'événement OnWorkCompleted je réactiver certains boutons, modifiez une propriété TextBox.Text et déclenchez un événement pour le formulaire parent.BackgroundWorker OnWorkCompleted lève l'exception de thread-thread

Le formulaire A contient mon UserControl. Quand je clique sur un bouton qui ouvre le formulaire B, même si je ne fais rien "là" et que je le ferme juste, et que j'essaie d'introduire la page suivante de ma base de données, le OnWorkCompleted est appelé (et non mon fil principal), et jette une exception de thread-thread.

Au moment où j'ai ajouté une vérification pour InvokeRequired au gestionnaire là, mais le point entier de OnWorkCompleted ne doit-il pas être appelé sur le fil principal? Pourquoi cela ne fonctionnerait-il pas comme prévu?

EDIT:

J'ai réussi à limiter le problème à arcgis et BackgroundWorker. J'ai la solution suivante qui ajoute une commande à arcmap, qui ouvre un simple Form1 avec deux boutons.

Le premier bouton exécute un BackgroundWorker qui dort pendant 500ms et met à jour un compteur. Dans la méthode RunWorkerCompleted, il vérifie InvokeRequired et met à jour le titre pour montrer si la méthode était initialement utilisée dans le thread principal ou le thread de travail. Le deuxième bouton ouvre simplement Form2, qui ne contient rien.

Au début, tous les appels à RunWorkerCompletedare sont faites à l'intérieur du fil conducteur (Comme prévu - thats le point de whold de la méthode RunWorkerComplete, au moins par ce que je comprends de la MSDN sur BackgroundWorker)

Après ouverture et En fermant Form2, le RunWorkerCompleted est toujours appelé sur le thread de travail. Je veux ajouter que je peux juste laisser cette solution au problème tel quel (vérifier InvokeRequired dans la méthode RunWorkerCompleted), mais je veux comprendre pourquoi cela se passe contre mes attentes. Dans mon "vrai" code je voudrais toujours savoir que la méthode RunWorkerCompleted est appelée sur le thread principal.

j'ai réussi à détecter le problème à la commande form.Show(); dans mon BackgroundTesterBtn - si j'utilise ShowDialog() à la place, je reçois pas de problème (RunWorkerCompleted fonctionne toujours sur le thread principal). J'ai besoin d'utiliser Show() dans mon projet ArcMap, afin que l'utilisateur ne soit pas lié au formulaire. J'ai également essayé de reproduire le bug sur un projet WinForms normal. J'ai ajouté un projet simple qui ouvre juste le premier formulaire sans ArcMap, mais dans ce cas, je ne pouvais pas reproduire le bogue - le RunWorkerCompleted a couru sur le fil principal, que j'aie utilisé Show() ou ShowDialog(), avant et après l'ouverture Form2. J'ai essayé d'ajouter un troisième formulaire pour agir comme une forme principale avant mon Form1, mais cela n'a pas changé le résultat.

Here est mon simple, sln (VS2005sp1) - il faut

ESRI.ArcGIS.ADF (9.2.4.1420)

ESRI.ArcGIS.ArcMapUI (9.2.3.1380)

ESRI.ArcGIS.SystemUI (9.2.3.1380)

Répondre

7

n'est pas tout point de OnWorkCompleted doit être appelé sur le thread principal? Pourquoi cela ne fonctionnerait-il pas comme prévu?

Non, ce n'est pas le cas.
Vous ne pouvez pas exécuter n'importe quoi sur un ancien thread. Les threads ne sont pas des objets polis que vous pouvez simplement dire "exécutez ceci, s'il vous plaît".

Un meilleur modèle mental d'un fil est un train de marchandises. Une fois que c'est parti, c'est sur sa propre piste. Vous ne pouvez pas changer son cours ou l'arrêter. Si vous voulez l'influencer, attendez qu'il arrive à la gare suivante (par exemple: vérifiez manuellement certains événements) ou faites dérailler (Thread.Abort) et les exceptions CrossThread ont les mêmes conséquences que le déraillement d'un train. .. il faut se méfier!).

Winforms commande sorte de soutien ce comportement (Ils ont Control.BeginInvoke qui vous permet d'exécuter une fonction sur le thread d'interface utilisateur), mais cela ne fonctionne que parce qu'ils ont un crochet spécial dans la fenêtre interface utilisateur pompe de messages et d'écrire des gestionnaires spéciaux . Pour aller avec l'analogie ci-dessus, leur train vérifie à la station et cherche de nouvelles directions périodiquement, et vous pouvez utiliser cette facilité pour afficher vos propres directions.

Le BackgroundWorker est conçu pour être général (il ne peut pas être lié à l'interface graphique de Windows) donc il ne peut pas utiliser les fonctionnalités de Windows Control.BeginInvoke. Il doit supposer que votre thread principal est un «train» imparable faisant son propre truc, donc l'événement terminé doit s'exécuter dans le thread de travail ou pas du tout.

Cependant, comme vous utilisez WinForms, dans votre gestionnaire OnWorkCompleted, vous pouvez obtenir la fenêtre pour exécuter un autre rappel en utilisant la fonctionnalité BeginInvoke je l'ai mentionné ci-dessus. Comme ceci:

// Assume we're running in a windows forms button click so we have access to the 
// form object in the "this" variable. 
void OnButton_Click(object sender, EventArgs e) 
    var b = new BackgroundWorker(); 
    b.DoWork += ... blah blah 

    // attach an anonymous function to the completed event. 
    // when this function fires in the worker thread, it will ask the form (this) 
    // to execute the WorkCompleteCallback on the UI thread. 
    // when the form has some spare time, it will run your function, and 
    // you can do all the stuff that you want 
    b.RunWorkerCompleted += (s, e) { this.BeginInvoke(WorkCompleteCallback); } 
    b.RunWorkerAsync(); // GO! 
} 

void WorkCompleteCallback() 
{ 
    Button.Enabled = false; 
    //other stuff that only works in the UI thread 
} 

Also, don't forget this:

Votre gestionnaire d'événements RunWorkerCompleted doit toujours vérifier l'erreur et les propriétés annulées avant l'accès à la propriété Result. Si une exception a été générée ou si l'opération a été annulée, l'accès à la propriété Résultat déclenche une exception.

+0

Vérification msdn sur BackgroundWorker Je vois que je spécifiquement devrait utiliser l'événement RunWorkerCompleted pour gérer le formulaire. "Vous devez veiller à ne manipuler aucun objet de l'interface utilisateur dans votre gestionnaire d'événements DoWork, mais à communiquer à l'interface utilisateur via les événements ProgressChanged et RunWorkerCompleted." également - chaque fois que j'essaie un scénario plus simple, cela fonctionne bien et est appelé sur le fil principal. Comme dans l'exemple msdn là, ou la plupart du temps dans ma demande. C'est similaire à www.developmentnow.com/g/36_2008_3_0_0_1056508/Update-GUI-from-RunWorkerCompleted.htm –

+0

C'est intéressant ... J'ai créé une application de test (application console) avec un simple travailleur de fond, et ça a déclenché chez le travailleur fil. Peut-être que le constructeur BackgroundWorker a de la magie qui le fait changer de comportement s'il est créé sur un thread d'interface utilisateur ou non? –

2

Les BackgroundWorker vérifie si l'instance de délégué, les points à une classe qui supporte l'interface ISynchronizeInvoke. Votre couche DAL n'implémente probablement pas cette interface. Normalement, vous utiliserez le BackgroundWorker sur un Form, qui prend en charge cette interface.

Si vous souhaitez utiliser la BackgroundWorker de la couche DAL et que vous souhaitez mettre à jour l'interface utilisateur à partir de là, vous avez trois options:

  • vous restiez appeler la méthode Invoke
  • implémenter l'interface sur la classe DAL et redirige les appels manuellement (trois méthodes et une propriété seulement)
  • avant d'appeler le BackgroundWorker (donc, sur le thread UI), d'appeler SynchronizationContext.Current et d'enregistrer l'instance de contenu dans une variable d'instance. Le SynchronizationContext vous donnera alors la méthode Send, qui fera exactement ce que fait Invoke.
+0

J'utilise BackgroundWorker dans un UserControl (qui est ISyncornizeInvoke). Mon contrôleur de radiomessagerie ne sait pas qu'il s'exécute dans un thread de travail, et cela ne l'intéresse pas non plus. Il exécute simplement une requête de pagination. L'exception est levée dans UserControl, à partir de l'événement OnWorkCompleted. –

1

La meilleure approche pour éviter les problèmes de cross-threading dans GUI est de use SynchronizationContext.

+0

C'est un super article, merci pour le lien ... –

Questions connexes