Question

J'ai un simple UserControl pour la pagination de base de données, qui utilise un contrôleur pour effectuer les appels DAL réels. J'utilise BackgroundWorker pour effectuer le gros travail, et sur l'événement OnWorkCompleted , je réactive certains boutons, modifie une propriété TextBox.Text et déclencher un événement pour le formulaire parent.

Le formulaire A contient mon UserControl. Lorsque je clique sur un bouton qui ouvre le formulaire B, même si je ne fais rien "là-bas" et fermez-le, et essayez d’apporter la page suivante de ma base de données, le OnWorkCompleted est appelé sur le thread de travail (et non sur mon thread principal), et lève une exception cross-thread.

Pour le moment, j'ai ajouté une coche pour InvokeRequired dans le gestionnaire, mais le point entier de OnWorkCompleted ne doit-il pas être appelé sur le thread 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 500 ms et met à jour un compteur. Dans la méthode RunWorkerCompleted , il recherche InvokeRequired et met à jour le titre pour indiquer si la méthode était exécutée à l'origine dans le thread principal ou dans le thread de travail. Le deuxième bouton ouvre simplement Form2 , qui ne contient rien.

Au début, tous les appels à RunWorkerCompletedare sont passés à l'intérieur du thread principal (Comme prévu - c'est le point le plus lourd de la méthode RunWorkerComplete, Du moins d'après ce que j'ai compris de MSDN sur BackgroundWorker )

Après avoir ouvert et fermé Form2 , RunWorkerCompleted est toujours appelé sur le thread de travail. Je veux ajouter que je peux simplement laisser cette solution au problème telle quelle (vérifiez InvokeRequired dans la méthode RunWorkerCompleted ), mais je veux comprendre pourquoi cela se produit. mes attentes. Dans mon " réel " code J'aimerais toujours savoir que la méthode RunWorkerCompleted est appelée sur le thread principal.

J'ai réussi à identifier le problème au niveau de la commande form.Show (); dans mon BackgroundTesterBtn - si j'utilise ShowDialog () au lieu de cela, je n’ai aucun problème ( RunWorkerCompleted s’exécute 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 bogue sur un projet WinForms normal. J'ai ajouté un projet simple qui ouvre simplement le premier formulaire sans ArcMap, mais dans ce cas, je ne pouvais pas reproduire le bogue - le RunWorkerCompleted était exécuté sur le thread principal, que j'avais utilisé Show () ou non. ou ShowDialog () , avant et après l'ouverture de Form2 . J'ai essayé d'ajouter un troisième formulaire pour qu'il agisse comme formulaire principal avant mon Form1 , mais cela n'a pas modifié le résultat.

Voici mon sln simple (VS2005sp1) - il est nécessaire de

ESRI.ArcGIS.ADF (9.2.4.1420)

ESRI.ArcGIS.ArcMapUI (9.2.3.1380)

ESRI.ArcGIS.SystemUI (9.2.3.1380)

Était-ce utile?

La solution

Cela ressemble à un bug:

http://connect.microsoft.com/VisualStudio/feedback /ViewFeedback.aspx?FeedbackID=116930

http://thedatafarm.com/devlifeblog/archive/2005 /12/21/39532.aspx

Je suggère donc d'utiliser le pare-balles (pseudocode):

if(control.InvokeRequired)
  control.Invoke(Action);
else
  Action()

Autres conseils

  

Le point entier de OnWorkCompleted ne doit-il pas être appelé sur le thread principal? Pourquoi cela ne fonctionnerait-il pas comme prévu?

Non, ce n'est pas.
Vous ne pouvez pas simplement lancer une vieille chose sur un vieux fil. 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 de fil est un train de marchandises. Une fois que c'est parti, c'est sur sa propre piste. Vous ne pouvez pas changer de cap ou l'arrêter. Si vous voulez l’influencer, vous devez attendre jusqu’à ce qu’elle arrive à la gare suivante (par exemple: le faire vérifier manuellement pour certains événements) ou le faire dérailler ( Thread.Abort et les exceptions CrossThread ont presque les mêmes conséquences que de faire dérailler un train ... méfiez-vous!).

Les contrôles Winforms prennent en charge ce problème avec une sorte (ils ont Control.BeginInvoke qui vous permet d'exécuter n'importe quelle fonction sur le thread d'interface utilisateur), mais cela ne fonctionne que parce qu'ils ont crochet spécial dans la pompe de message de l'interface utilisateur Windows et écrivez des gestionnaires spéciaux. Pour reprendre l’analogie ci-dessus, leur train s’enregistre à la gare et recherche périodiquement de nouvelles indications. Vous pouvez utiliser cette fonction pour l’afficher vous-même.

BackgroundWorker est conçu pour un usage général (il ne peut pas être lié à l'interface graphique de Windows) et ne peut donc pas utiliser les fonctionnalités Windows Control.BeginInvoke . Il doit supposer que votre fil principal est un "train" imparable qui agit comme il se doit, donc l'événement terminé doit s'exécuter dans le fil de travail ou pas du tout.

Toutefois, lorsque vous utilisez Winforms, dans votre gestionnaire OnWorkCompleted , vous pouvez demander à la fenêtre d'exécuter un autre rappel à l'aide du BeginInvoke . fonctionnalité que j'ai mentionnée 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
}

N'oubliez pas ceci:

  

Votre gestionnaire d'événements RunWorkerCompleted doit toujours vérifier les propriétés Error et Canceled avant d'accéder à la propriété Result. Si une exception a été déclenchée ou si l'opération a été annulée, l'accès à la propriété Result déclenche une exception.

BackgroundWorker vérifie si l'instance de délégué pointe vers une classe qui prend en charge l'interface ISynchronizeInvoke . Votre couche DAL n'implémente probablement pas cette interface. Normalement, vous utiliseriez BackgroundWorker sur un Form , qui prend en charge cette interface.

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

  • vous voudriez continuer à appeler la méthode Invoke
  • implémente l'interface ISynchronizeInvoke sur la classe DAL et redirige les appels manuellement (il ne s'agit que de trois méthodes et d'une propriété)
  • avant d'appeler BackgroundWorker (donc sur le thread d'interface utilisateur), pour appeler SynchronizationContext.Current et pour enregistrer l'instance de contenu dans une variable d'instance. Le SynchronizationContext vous donnera ensuite la méthode Send , qui fera exactement ce que Invoke fait.

La meilleure approche pour éviter les problèmes de cross-threading dans l'interface graphique consiste à utilise SynchronizationContext .

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top