49 votes

Raise Events in .NET sur le thread principal de l'interface utilisateur

Je suis le développement d'une bibliothèque de classe dans .NET que d'autres développeurs vont consommer par la suite. Cette bibliothèque utilise un peu de threads de travail, et ces fils de feu des événements d'état qui sera la cause d'une INTERFACE utilisateur mise à jour des contrôles dans les WinForms / WPF application.

Normalement, à chaque mise à jour, vous devez vérifier l' .InvokeRequired de propriété sur les WinForms ou équivalent WPF propriété et invoquer cette sur-le-main thread d'INTERFACE utilisateur pour la mise à jour. Cela peut vieillir rapidement, et quelque chose ne se sent pas bien à propos de faire à la fin de développeur de le faire, donc...

Est-il possible que ma bibliothèque de feu/invoque les événements et les délégués de l'INTERFACE utilisateur principale thread?

En particulier,...

  1. Dois-je automatiquement "détecter" la "principale" de fil à utiliser?
  2. Si non, dois-je exiger la fin de développeur pour appeler un (pseudo) UseThisThreadForEvents() méthode lorsque l'application démarre, donc je peux attraper le thread cible de cet appel?

40voto

itowlson Points 44174

Votre bibliothèque a pu vérifier la Cible de chaque délégué dans le cas de l'invocation de la liste, et le maréchal de l'appel à la thread cible si la cible est ISynchronizeInvoke:

private void RaiseEventOnUIThread(Delegate theEvent, object[] args)
{
  foreach (Delegate d in theEvent.GetInvocationList())
  {
    ISynchronizeInvoke syncer = d.Target as ISynchronizeInvoke;
    if (syncer == null)
    {
      d.DynamicInvoke(args);
    }
    else
    {
      syncer.BeginInvoke(d, args);  // cleanup omitted
    }
  }
}

Une autre approche, ce qui rend l'enfilage contrat plus explicite, c'est pour obliger les clients de votre bibliothèque pour passer une ISynchronizeInvoke ou SynchronizationContext pour le thread sur lequel ils veulent vous pour déclencher des événements. Cela donne aux utilisateurs de votre bibliothèque un peu plus de visibilité et de contrôle que l' "secrètement vérifier le délégué de la cible" approche.

En ce qui concerne votre deuxième question, je mets en place le fil de l'ordonnancement des choses dans votre OnXxx ou que ce soit de l'API le code de l'utilisateur des appels qui pourraient résulter d'un événement déclenché.

28voto

Mike Bouck Points 285

Voici l'idée d'Itwolson exprimée en tant que méthode d'extension qui fonctionne très bien pour moi:

 /// <summary>Extension methods for EventHandler-type delegates.</summary>
public static class EventExtensions
{
    /// <summary>Raises the event (on the UI thread if available).</summary>
    /// <param name="multicastDelegate">The event to raise.</param>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">An EventArgs that contains the event data.</param>
    /// <returns>The return value of the event invocation or null if none.</returns>
    public static object Raise(this MulticastDelegate multicastDelegate, object sender, EventArgs e)
    {
        object retVal = null;

        MulticastDelegate threadSafeMulticastDelegate = multicastDelegate;
        if (threadSafeMulticastDelegate != null)
        {
            foreach (Delegate d in threadSafeMulticastDelegate.GetInvocationList())
            {
                var synchronizeInvoke = d.Target as ISynchronizeInvoke;
                if ((synchronizeInvoke != null) && synchronizeInvoke.InvokeRequired)
                {
                    retVal = synchronizeInvoke.EndInvoke(synchronizeInvoke.BeginInvoke(d, new[] { sender, e }));
                }
                else
                {
                    retVal = d.DynamicInvoke(new[] { sender, e });
                }
            }
        }

        return retVal;
    }
}
 

Vous venez alors de soulever votre événement comme suit:

 MyEvent.Raise(this, EventArgs.Empty);
 

13voto

SLaks Points 391154

Vous pouvez utiliser la classe SynchronizationContext pour regrouper les appels du thread d'interface utilisateur dans WinForms ou WPF en utilisant SynchronizationContext.Current .

11voto

hoodaticus Points 875

J'ai aimé Mike Bouk réponse (+1), de sorte beaucoup, je l'ai insérée dans mon code. Je suis inquiète de ce que son DynamicInvoke appel lèvera une exception d'exécution si le Délégué qu'il invoque n'est pas un Gestionnaire délégué, en raison de paramètres incompatibles. Et puisque vous êtes dans un thread d'arrière-plan, je suppose que vous pouvez appeler la méthode de l'INTERFACE utilisateur de manière asynchrone et que vous n'êtes pas concerné si il termine de se.

Ma version ci-dessous peut être utilisé uniquement avec ce Gestionnaire délégués et ignorera les autres délégués, dans sa liste d'invocation. Depuis ce Gestionnaire délégués retour rien, nous n'avons pas besoin du résultat. Cela me permet de m'appeler EndInvoke après le processus asynchrone se termine en passant par le Gestionnaire dans le BeginInvoke appel. L'appel sera de retour ce Gestionnaire d'événements dans IAsyncResult.AsyncState par le biais de la AsynchronousCallback, à ce point de Gestionnaire d'événements.EndInvoke est appelé.

/// <summary>
/// Safely raises any EventHandler event asynchronously.
/// </summary>
/// <param name="sender">The object raising the event (usually this).</param>
/// <param name="e">The EventArgs for this event.</param>
public static void Raise(this MulticastDelegate thisEvent, object sender, 
    EventArgs e)
{
  EventHandler uiMethod; 
  ISynchronizeInvoke target; 
  AsyncCallback callback = new AsyncCallback(EndAsynchronousEvent);

  foreach (Delegate d in thisEvent.GetInvocationList())
  {
    uiMethod = d as EventHandler;
    if (uiMethod != null)
    {
      target = d.Target as ISynchronizeInvoke; 
      if (target != null) target.BeginInvoke(uiMethod, new[] { sender, e }); 
      else uiMethod.BeginInvoke(sender, e, callback, uiMethod);
    }
  }
}

private static void EndAsynchronousEvent(IAsyncResult result) 
{ 
  ((EventHandler)result.AsyncState).EndInvoke(result); 
}

Et l'utilisation:

MyEventHandlerEvent.Raise(this, MyEventArgs);

5voto

antonm Points 1869

Vous pouvez stocker le répartiteur pour le thread principal de votre bibliothèque, l'utiliser pour vérifier si vous exécutez le thread d'interface utilisateur et l'exécuter si nécessaire.

La documentation de thread WPF fournit une bonne introduction et des exemples sur la façon de procéder.

En voici l'essentiel:

 private Dispatcher _uiDispatcher;

// Call from the main thread
public void UseThisThreadForEvents()
{
     _uiDispatcher = Dispatcher.CurrentDispatcher;
}

// Some method of library that may be called on worker thread
public void MyMethod()
{
    if (Dispatcher.CurrentDispatcher != _uiDispatcher)
    {
        _uiDispatcher.Invoke(delegate()
        {
            // UI thread code
        });
    }
    else
    {
         // UI thread code
    }
}
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X