Comment puis-je sécuriser les rappels d'événements dans mon thread Win forms?

Lorsque vous vous abonnez à un événement sur un objet à partir d'un formulaire, vous transmettez essentiellement le contrôle de votre méthode de rappel à la source de l'événement. Vous n'avez aucune idée si cette source d'événement choisira de déclencher l'événement sur un thread différent.

Le problème est que lorsque le rappel est appelé, vous ne pouvez pas supposer que vous pouvez faire des contrôles de mise à jour sur votre formulaire car parfois ces contrôles lanceront une expection si le rappel d'événement a été appelé sur un thread différent que le fil sur lequel le formulaire a été exécuté.

33
demandé sur Alex Miller 2008-08-08 21:32:41

6 réponses

Pour simplifier un peu le code de Simon, vous pouvez utiliser le délégué d'Action Générique intégré. Cela permet d'économiser votre code avec un tas de types de délégués dont vous n'avez pas vraiment besoin. En outre, dans. net 3.5, ils ont ajouté un paramètre params à la méthode Invoke afin que vous n'ayez pas à définir un tableau temporaire.

void SomethingHappened(object sender, EventArgs ea)
{
   if (InvokeRequired)
   {
      Invoke(new Action<object, EventArgs>(SomethingHappened), sender, ea);
      return;
   }

   textBox1.Text = "Something happened";
}
31
répondu Jake Pearson 2008-08-12 15:59:51

Voici les points saillants:

  1. vous ne pouvez pas effectuer D'appels de contrôle D'interface utilisateur à partir d'un thread différent de celui sur lequel ils ont été créés (le thread du formulaire).
  2. Délégué invocations (c'est à dire, événement crochets) sont déclenchées sur le même thread que l'objet qui déclenche l'événement.

Donc, si vous avez un thread "engine" séparé qui fait du travail et que vous avez une interface utilisateur qui surveille les changements d'état qui peuvent être reflétés dans l'interface utilisateur (comme une barre de progression ou autre), vous avez un problème. Le feu du moteur est un événement modifié par l'objet qui a été accroché par le formulaire. Mais le délégué de rappel que le formulaire enregistré avec le moteur est appelé sur le thread du moteur... pas sur le thread du formulaire. Et donc vous ne pouvez pas mettre à jour les contrôles de ce rappel. Doh!

BeginInvoke vient à la rescousse. Utilisez simplement ce modèle de codage simple dans toutes vos méthodes de rappel et vous pouvez être sûr que tout ira bien:

private delegate void EventArgsDelegate(object sender, EventArgs ea);

void SomethingHappened(object sender, EventArgs ea)
{
   //
   // Make sure this callback is on the correct thread
   //
   if (this.InvokeRequired)
   {
      this.Invoke(new EventArgsDelegate(SomethingHappened), new object[] { sender, ea });
      return;
   }

   //
   // Do something with the event such as update a control
   //
   textBox1.Text = "Something happened";
}

C'est assez simple vraiment.

  1. Utilisez InvokeRequired pour savoir si ce rappel s'est produit sur le thread correct.
  2. si ce n'est pas le cas, réinvitez le rappel sur le thread correct avec les mêmes paramètres. Vous pouvez réinventer une méthode en utilisant les méthodesInvoke (bloquant) ouBeginInvoke (non bloquant).
  3. La prochaine fois que la fonction est appelée, InvokeRequired renvoie false car nous sommes maintenant sur le bon thread et tout le monde est content.

C'est un moyen très compact de résoudre ce problème et de protéger vos formulaires contre les rappels d'événements multi-threads.

16
répondu Simon Gillbee 2008-08-08 17:35:40

J'utilise beaucoup de méthodes anonymes dans ce scénario:

void SomethingHappened(object sender, EventArgs ea)
{
   MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
   InvokeRequired ? Invoke( del ) : del(); 
}
9
répondu Jason Diller 2008-10-21 18:31:58

Je suis un peu en retard sur ce sujet, mais vous voudrez peut-être jeter un oeil au modèle asynchrone Basé sur les événements. Lorsqu'il est implémenté correctement, il garantit que les événements sont toujours déclenchés à partir du thread de L'interface utilisateur.

Voici un bref exemple qui n'autorise qu'une seule invocation simultanée; la prise en charge de plusieurs invocations/événements nécessite un peu plus de plomberie.

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class MainForm : Form
    {
        private TypeWithAsync _type;

        [STAThread()]
        public static void Main()
        {
            Application.EnableVisualStyles();
            Application.Run(new MainForm());
        }

        public MainForm()
        {
            _type = new TypeWithAsync();
            _type.DoSomethingCompleted += DoSomethingCompleted;

            var panel = new FlowLayoutPanel() { Dock = DockStyle.Fill };

            var btn = new Button() { Text = "Synchronous" };
            btn.Click += SyncClick;
            panel.Controls.Add(btn);

            btn = new Button { Text = "Asynchronous" };
            btn.Click += AsyncClick;
            panel.Controls.Add(btn);

            Controls.Add(panel);
        }

        private void SyncClick(object sender, EventArgs e)
        {
            int value = _type.DoSomething();
            MessageBox.Show(string.Format("DoSomething() returned {0}.", value));
        }

        private void AsyncClick(object sender, EventArgs e)
        {
            _type.DoSomethingAsync();
        }

        private void DoSomethingCompleted(object sender, DoSomethingCompletedEventArgs e)
        {
            MessageBox.Show(string.Format("DoSomethingAsync() returned {0}.", e.Value));
        }
    }

    class TypeWithAsync
    {
        private AsyncOperation _operation;

        // synchronous version of method
        public int DoSomething()
        {
            Thread.Sleep(5000);
            return 27;
        }

        // async version of method
        public void DoSomethingAsync()
        {
            if (_operation != null)
            {
                throw new InvalidOperationException("An async operation is already running.");
            }

            _operation = AsyncOperationManager.CreateOperation(null);
            ThreadPool.QueueUserWorkItem(DoSomethingAsyncCore);
        }

        // wrapper used by async method to call sync version of method, matches WaitCallback so it
        // can be queued by the thread pool
        private void DoSomethingAsyncCore(object state)
        {
            int returnValue = DoSomething();
            var e = new DoSomethingCompletedEventArgs(returnValue);
            _operation.PostOperationCompleted(RaiseDoSomethingCompleted, e);
        }

        // wrapper used so async method can raise the event; matches SendOrPostCallback
        private void RaiseDoSomethingCompleted(object args)
        {
            OnDoSomethingCompleted((DoSomethingCompletedEventArgs)args);
        }

        private void OnDoSomethingCompleted(DoSomethingCompletedEventArgs e)
        {
            var handler = DoSomethingCompleted;

            if (handler != null) { handler(this, e); }
        }

        public EventHandler<DoSomethingCompletedEventArgs> DoSomethingCompleted;
    }

    public class DoSomethingCompletedEventArgs : EventArgs
    {
        private int _value;

        public DoSomethingCompletedEventArgs(int value)
            : base()
        {
            _value = value;
        }

        public int Value
        {
            get { return _value; }
        }
    }
}
2
répondu OwenP 2008-12-04 20:08:40

En tant que lazy programmer, j'ai une méthode très paresseuse pour le faire.

Ce que je fais est simplement ceci.

private void DoInvoke(MethodInvoker del) {
    if (InvokeRequired) {
        Invoke(del);
    } else {
        del();
    }
}
//example of how to call it
private void tUpdateLabel(ToolStripStatusLabel lbl, String val) {
    DoInvoke(delegate { lbl.Text = val; });
}

Vous pouvez intégrer le DoInvoke dans votre fonction ou le cacher dans une fonction séparée pour faire le sale boulot pour vous.

Gardez à l'esprit que vous pouvez passer des fonctions directement dans la méthode DoInvoke.

private void directPass() {
    DoInvoke(this.directInvoke);
}
private void directInvoke() {
    textLabel.Text = "Directly passed.";
}
1
répondu Chase 2012-05-30 21:57:58

Dans de nombreux cas simples, vous pouvez utiliser le délégué MethodInvoker et éviter la nécessité de créer votre propre type de délégué.

0
répondu Chris Farmer 2008-08-08 17:41:18