Comment puis-je rendre les rappels d'événements dans mes formulaires gagnants en toute sécurité ?

StackOverflow https://stackoverflow.com/questions/6184

Question

Lorsque vous vous abonnez à un événement sur un objet à partir d'un formulaire, vous confiez essentiellement le contrôle de votre méthode de rappel à la source de l'événement.Vous ne savez pas si cette source d'événement choisira de déclencher l'événement sur un autre thread.

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

Était-ce utile?

La solution

Pour simplifier un peu le code de Simon, vous pouvez utiliser le délégué Action générique intégré.Cela évite de parsemer votre code avec un tas de types de délégués dont vous n'avez pas vraiment besoin.De plus, 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";
}

Autres conseils

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. Les invocations de délégués (c'est-à-dire les hooks d'événement) sont déclenchées sur le même thread que l'objet qui déclenche l'événement.

Donc, si vous avez un thread « moteur » distinct qui effectue un travail et qu'une interface utilisateur 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.L'incendie du moteur est un événement de changement d'objet qui a été accroché par le formulaire.Mais le délégué de rappel que le formulaire enregistré auprès du moteur est appelé sur le fil du moteur… pas sur le fil du formulaire.Et vous ne pouvez donc mettre à jour aucun contrôle à partir de ce rappel.Oh !

CommencerInvoquer 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 vraiment très simple.

  1. Utiliser InvocationObligatoire pour savoir si ce rappel s'est produit sur le bon fil de discussion.
  2. Sinon, réinvoquez le rappel sur le bon thread avec les mêmes paramètres.Vous pouvez réinvoquer une méthode en utilisant le Invoquer (blocage) ou CommencerInvoquer méthodes (non bloquantes).
  3. La prochaine fois que la fonction sera appelée, InvocationObligatoire renvoie false car nous sommes maintenant sur le bon fil et tout le monde est content.

Il s'agit d'un moyen très compact de résoudre ce problème et de protéger vos formulaires contre les rappels d'événements multithread.

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

J'arrive un peu en retard sur ce sujet, mais vous voudrez peut-être jeter un œil 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'un seul appel simultané ;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; }
        }
    }
}

Comme le lazy programmer, j'ai une méthode très paresseuse pour faire cela.

Ce que je fais, c'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 masquer dans une fonction distincte pour faire le sale boulot à votre place.

Gardez simplement à l’esprit que vous pouvez transmettre des fonctions directement dans la méthode DoInvoke.

private void directPass() {
    DoInvoke(this.directInvoke);
}
private void directInvoke() {
    textLabel.Text = "Directly passed.";
}

Dans de nombreux cas simples, vous pouvez utiliser le délégué MethodInvoker et éviter d'avoir à créer votre propre type de délégué.

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