web-dev-qa-db-fra.com

Vous évitez les malheurs d'Invoke/BeginInvoke dans la gestion d'événements multithreads?

Je suis toujours aux prises avec un thread d'arrière-plan dans une interface utilisateur WinForm. Pourquoi? Voici quelques-uns des problèmes:

  1. De toute évidence, le problème le plus important, je ne peux pas modifier un contrôle à moins que je n’exécute sur le même thread qui l’a créé.
  2. Comme vous le savez, Invoke, BeginInvoke, etc. ne sont disponibles qu'après la création d'un contrôle.
  3. Même après le retour de la valeur requise à RequinsInvoke, BeginInvoke peut toujours lancer ObjectDisposed et, même s'il ne le fait pas, il ne peut jamais exécuter le code si le contrôle est en cours de destruction.
  4. Même après le retour de MUSTInVoke à true, Invoke peut suspendre indéfiniment l'attente de l'exécution par un contrôle supprimé en même temps que l'appel à Invoke.

Je cherche une solution élégante à ce problème, mais avant d'entrer dans les détails de ce que je cherche, je pensais pouvoir clarifier le problème. Il s’agit de prendre le problème générique et de mettre un exemple plus concret derrière. Pour cet exemple, disons que nous transférons de plus grandes quantités de données sur Internet. L'interface utilisateur doit pouvoir afficher une boîte de dialogue de progression pour le transfert déjà en cours. Le dialogue de progression doit être mis à jour constamment et rapidement (mises à jour 5 à 20 fois par seconde). L'utilisateur peut fermer la boîte de dialogue de progression à tout moment et la rappeler si nécessaire. Et de plus, supposons, pour des arguments, que si la boîte de dialogue est visible, elle doit traiter chaque événement de progression. L'utilisateur peut cliquer sur Annuler dans la boîte de dialogue de progression et, en modifiant l'opération args, annuler l'opération.

Maintenant, j'ai besoin d'une solution qui s'intègre dans la boîte de contraintes suivante:

  1. Autoriser un thread de travail à appeler une méthode sur un contrôle/formulaire et bloquer/attendre la fin de l'exécution.
  2. Autoriser le dialogue lui-même à appeler cette même méthode à l'initialisation ou similaire (et donc à ne pas utiliser invoke).
  3. N'imposez aucune charge d'implémentation à la méthode de traitement ou à l'événement appelant. La solution ne doit modifier que l'abonnement d'événement lui-même.
  4. Gérez correctement le blocage des invocations dans une boîte de dialogue en cours d'élimination. Malheureusement, ce n'est pas aussi facile que de rechercher IsDisposed.
  5. Doit pouvoir être utilisé avec n'importe quel type d'événement (supposons un délégué de type EventHandler)
  6. Ne doit pas traduire les exceptions en TargetInvocationException.
  7. La solution doit fonctionner avec .Net 2.0 et supérieur

Alors, cela peut-il être résolu compte tenu des contraintes ci-dessus? J'ai fouillé et fouillé dans d'innombrables blogs et discussions et, hélas, je suis toujours les mains vides.

Mise à jour: Je me rends compte que cette question n'a pas de réponse facile. Je ne suis sur ce site que depuis quelques jours et j'ai vu des personnes très expérimentées répondre à des questions. J'espère que l'une de ces personnes aura résolu le problème suffisamment pour que je ne passe pas la semaine environ, il faudra donc trouver une solution raisonnable.

Mise à jour n ° 2: Ok, je vais essayer de décrire le problème un peu plus en détail et de voir ce qui se produit (le cas échéant). Les propriétés suivantes qui nous permettent de déterminer son état soulèvent quelques inquiétudes:.

  1. Control.InvokeRequired = Documenté pour renvoyer false si exécuté sur le thread actuel ou si IsHandleCreated renvoie false pour tous les parents. Je suis troublé par l'implémentation InvokeRequired pouvant potentiellement émettre une exception ObjectDisposedException ou potentiellement recréer le descripteur de l'objet. Et puisque InvokeRequired peut renvoyer true lorsque nous ne sommes pas en mesure d'invoquer (Dispose en cours) et false même s'il peut être nécessaire d'utiliser invoke (Créer en cours), cela ne peut tout simplement pas être approuvé dans tous les cas. Le seul cas dans lequel nous pouvons faire confiance à InvokeRequired en renvoyant false est lorsque IsHandleCreated renvoie true avant et après l'appel (BTW, la documentation MSDN pour InvokeRequired mentionne la vérification de IsHandleCreated).

  2. Control.IsHandleCreated = Retourne true si un descripteur a été attribué au contrôle; sinon, faux. Bien que IsHandleCreated soit un appel sécurisé, il peut échouer si le contrôle est en train de recréer son handle. Ce problème potentiel semble pouvoir être résolu en effectuant un verrou (contrôle) lors de l'accès à IsHandleCreated et InvokeRequired.

  3. Control.Disposing = Retourne true si le contrôle est en cours de destruction.

  4. Control.IsDisposed = Retourne true si le contrôle a été supprimé. J'envisage de souscrire à l'événement Disposed et de vérifier la propriété IsDisposed pour déterminer si BeginInvoke sera un jour terminée. Le gros problème ici est l’absence de verrouillage de la synchronisation pendant la transition Disposition -> Disposition. Il est possible que si vous vous abonnez à l'événement Disposed et que vous vérifiiez par la suite que la mise au rebut == false && IsDisposed == false ne soit toujours pas déclenchée. Cela est dû au fait que l'implémentation de Dispose définit Disposing = false, puis définit Disposed = true. Ceci vous fournit une opportunité (aussi petite soit-elle) de lire à la fois Disposing et IsDisposed comme faux sur un contrôle éliminé.

... ma tête me fait mal :( J'espère que les informations ci-dessus apporteront un éclairage supplémentaire sur les problèmes qui se posent à quiconque ayant ces problèmes. J'apprécie vos cycles de réflexion sur ce sujet.

Nous nous rapprochons du problème ... Voici la deuxième moitié de la méthode Control.DestroyHandle ():

if (!this.RecreatingHandle && (this.threadCallbackList != null))
{
    lock (this.threadCallbackList)
    {
        Exception exception = new ObjectDisposedException(base.GetType().Name);
        while (this.threadCallbackList.Count > 0)
        {
            ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue();
            entry.exception = exception;
            entry.Complete();
        }
    }
}
if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0)
{
    UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero);
}
else
{
    this.window.DestroyHandle();
}

Vous remarquerez que l'exception ObjectDisposedException est envoyée à tous les appels inter-thread en attente. Peu de temps après, l'appel à this.window.DestroyHandle () détruit à son tour la fenêtre et définit sa référence de poignée sur IntPtr.Zero, empêchant ainsi d'autres appels à la méthode BeginInvoke (ou plus précisément MarshaledInvoke qui gère à la fois BeginInvoke et Invoke). Le problème ici est que, après le déverrouillage du verrou sur threadCallbackList, une nouvelle entrée peut être insérée avant que le thread du contrôle zéros le handle de fenêtre. Cela semble être le cas que je vois, bien que rarement, assez souvent pour arrêter une libération.

Mise à jour # 4:

Désolé de continuer à traîner ça Cependant, je pensais que cela valait la peine de documenter ici. J'ai réussi à résoudre la plupart des problèmes ci-dessus et je cherche une solution qui fonctionne. Je suis tombé sur un autre problème qui m'inquiétait, mais jusqu'à présent, je n'avais pas vu «dans la nature».

Ce problème concerne le génie qui a écrit la propriété Control.Handle:

    public IntPtr get_Handle()
    {
        if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
        {
            throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
        }
        if (!this.IsHandleCreated)
        {
            this.CreateHandle();
        }
        return this.HandleInternal;
    }

Ce n'est pas si grave en soi (peu importe mon opinion sur les modifications de {}); cependant, lorsqu'il est combiné à la propriété InvokeRequired ou à la méthode Invoke/BeginInvoke, il est incorrect. Voici le flux de base de l'invocation:

if( !this.IsHandleCreated )
    throw;
... do more stuff
PostMessage( this.Handle, ... );

Le problème ici est que, à partir d'un autre thread, je peux passer avec succès à la première instruction if, après quoi le descripteur est détruit par le thread du contrôle, ce qui a pour effet que l'objet de la propriété Handle recrée le handle de fenêtre sur mon thread. Cela peut alors provoquer une exception sur le thread du contrôle d'origine. Celui-ci m'a vraiment laissé perplexe car il n'y a aucun moyen de se prémunir contre cela. S'ils utilisaient uniquement la propriété InternalHandle et testaient le résultat de IntPtr.Zero, cela ne poserait pas de problème.

48
csharptest.net

Votre scénario, tel que décrit, correspond parfaitement à BackgroundWorker - pourquoi ne pas simplement l'utiliser? Vos exigences en matière de solution sont trop génériques et plutôt déraisonnables - je doute qu’une solution puisse les satisfaire toutes.

22
Pavel Minaev

Il y a quelque temps, j'ai rencontré ce problème et j'ai trouvé une solution impliquant des contextes de synchronisation. La solution consiste à ajouter une méthode d'extension à SynchronizationContext qui lie un délégué particulier au thread auquel le SynchronizationContext est lié. Il générera un nouveau délégué qui, lorsqu'il sera appelé, organisera l'appel du thread approprié et appellera ensuite le délégué d'origine. Il est presque impossible pour les consommateurs du délégué de l'appeler dans le mauvais contexte.

Article de blog sur le sujet:

8
JaredPar

Ok, quelques jours plus tard, j'ai fini de créer une solution. Il résout toutes les contraintes et objectifs énumérés dans le post initial. L'utilisation est simple et directe:

myWorker.SomeEvent += new EventHandlerForControl<EventArgs>(this, myWorker_SomeEvent).EventHandler;

Lorsque le thread de travail appelle cet événement, il gérera l'appel requis vers le thread de contrôle. Cela garantit qu’il ne se bloque pas indéfiniment et lève systématiquement une exception ObjectDisposedException s’il ne parvient pas à s’exécuter sur le thread de contrôle. J'ai créé d'autres dérivations de la classe, une pour ignorer l'erreur et une autre pour appeler directement le délégué si le contrôle n'est pas disponible. Semble bien fonctionner et passe pleinement les nombreux tests qui reproduisent les problèmes ci-dessus. Il n'y a qu'un seul problème avec la solution que je ne peux pas empêcher sans violer la contrainte 3 ci-dessus. Ce problème est le dernier (Mise à jour 4) de la description du problème, les problèmes de threads dans get Handle. Cela peut entraîner un comportement inattendu sur le thread de contrôle d'origine et j'ai régulièrement vu InvalidOperationException () levé lors de l'appel de Dispose () depuis le descripteur en cours de création sur mon thread. Pour permettre de gérer cela, je m'assure de verrouiller l'accès aux fonctions qui utiliseront la propriété Control.Handle. Cela permet à un formulaire de surcharger la méthode DestroyHandle et de le verrouiller avant d'appeler l'implémentation de base. Si cela est fait, cette classe devrait être entièrement thread-safe (à ma connaissance). 

public class Form : System.Windows.Forms.Form
{
    protected override void DestroyHandle()
    {
        lock (this) base.DestroyHandle();
    }
}

Vous remarquerez peut-être que l’aspect essentiel de la résolution du blocage des verrous est devenu une boucle de scrutation. A l'origine, j'ai résolu avec succès les cas de test en gérant l'événement du contrôle pour Disposed et HandleDestroyed et en utilisant plusieurs handles d'attente. Après un examen plus approfondi, j'ai trouvé que l'abonnement/désabonnement de ces événements n'était pas thread-safe. Ainsi, j'ai choisi d'interroger IsHandleCreated à la place afin de ne pas créer de conflit inutile sur les événements du thread et ainsi éviter la possibilité de produire encore un état de blocage total.

Quoi qu'il en soit, voici la solution que j'ai proposée:

/// <summary>
/// Provies a wrapper type around event handlers for a control that are safe to be
/// used from events on another thread.  If the control is not valid at the time the
/// delegate is called an exception of type ObjectDisposedExcpetion will be raised.
/// </summary>
[System.Diagnostics.DebuggerNonUserCode]
public class EventHandlerForControl<TEventArgs> where TEventArgs : EventArgs
{
    /// <summary> The control who's thread we will use for the invoke </summary>
    protected readonly Control _control;
    /// <summary> The delegate to invoke on the control </summary>
    protected readonly EventHandler<TEventArgs> _delegate;

    /// <summary>
    /// Constructs an EventHandler for the specified method on the given control instance.
    /// </summary>
    public EventHandlerForControl(Control control, EventHandler<TEventArgs> handler)
    {
        if (control == null) throw new ArgumentNullException("control");
        _control = control.TopLevelControl;
        if (handler == null) throw new ArgumentNullException("handler");
        _delegate = handler;
    }

    /// <summary>
    /// Constructs an EventHandler for the specified delegate converting it to the expected
    /// EventHandler&lt;TEventArgs> delegate type.
    /// </summary>
    public EventHandlerForControl(Control control, Delegate handler)
    {
        if (control == null) throw new ArgumentNullException("control");
        _control = control.TopLevelControl;
        if (handler == null) throw new ArgumentNullException("handler");

        //_delegate = handler.Convert<EventHandler<TEventArgs>>();
        _delegate = handler as EventHandler<TEventArgs>;
        if (_delegate == null)
        {
            foreach (Delegate d in handler.GetInvocationList())
            {
                _delegate = (EventHandler<TEventArgs>) Delegate.Combine(_delegate,
                    Delegate.CreateDelegate(typeof(EventHandler<TEventArgs>), d.Target, d.Method, true)
                );
            }
        }
        if (_delegate == null) throw new ArgumentNullException("_delegate");
    }


    /// <summary>
    /// Used to handle the condition that a control's handle is not currently available.  This
    /// can either be before construction or after being disposed.
    /// </summary>
    protected virtual void OnControlDisposed(object sender, TEventArgs args)
    {
        throw new ObjectDisposedException(_control.GetType().Name);
    }

    /// <summary>
    /// This object will allow an implicit cast to the EventHandler&lt;T> type for easier use.
    /// </summary>
    public static implicit operator EventHandler<TEventArgs>(EventHandlerForControl<TEventArgs> instance)
    { return instance.EventHandler; }

    /// <summary>
    /// Handles the 'magic' of safely invoking the delegate on the control without producing
    /// a dead-lock.
    /// </summary>
    public void EventHandler(object sender, TEventArgs args)
    {
        bool requiresInvoke = false, hasHandle = false;
        try
        {
            lock (_control) // locked to avoid conflicts with RecreateHandle and DestroyHandle
            {
                if (true == (hasHandle = _control.IsHandleCreated))
                {
                    requiresInvoke = _control.InvokeRequired;
                    // must remain true for InvokeRequired to be dependable
                    hasHandle &= _control.IsHandleCreated;
                }
            }
        }
        catch (ObjectDisposedException)
        {
            requiresInvoke = hasHandle = false;
        }

        if (!requiresInvoke && hasHandle) // control is from the current thread
        {
            _delegate(sender, args);
            return;
        }
        else if (hasHandle) // control invoke *might* work
        {
            MethodInvokerImpl invocation = new MethodInvokerImpl(_delegate, sender, args);
            IAsyncResult result = null;
            try
            {
                lock (_control)// locked to avoid conflicts with RecreateHandle and DestroyHandle
                    result = _control.BeginInvoke(invocation.Invoker);
            }
            catch (InvalidOperationException)
            { }

            try
            {
                if (result != null)
                {
                    WaitHandle handle = result.AsyncWaitHandle;
                    TimeSpan interval = TimeSpan.FromSeconds(1);
                    bool complete = false;

                    while (!complete && (invocation.MethodRunning || _control.IsHandleCreated))
                    {
                        if (invocation.MethodRunning)
                            complete = handle.WaitOne();//no need to continue polling once running
                        else
                            complete = handle.WaitOne(interval);
                    }

                    if (complete)
                    {
                        _control.EndInvoke(result);
                        return;
                    }
                }
            }
            catch (ObjectDisposedException ode)
            {
                if (ode.ObjectName != _control.GetType().Name)
                    throw;// *likely* from some other source...
            }
        }

        OnControlDisposed(sender, args);
    }

    /// <summary>
    /// The class is used to take advantage of a special-case in the Control.InvokeMarshaledCallbackDo()
    /// implementation that allows us to preserve the exception types that are thrown rather than doing
    /// a delegate.DynamicInvoke();
    /// </summary>
    [System.Diagnostics.DebuggerNonUserCode]
    private class MethodInvokerImpl
    {
        readonly EventHandler<TEventArgs> _handler;
        readonly object _sender;
        readonly TEventArgs _args;
        private bool _received;

        public MethodInvokerImpl(EventHandler<TEventArgs> handler, object sender, TEventArgs args)
        {
            _received = false;
            _handler = handler;
            _sender = sender;
            _args = args;
        }

        public MethodInvoker Invoker { get { return this.Invoke; } }
        private void Invoke() { _received = true; _handler(_sender, _args); }

        public bool MethodRunning { get { return _received; } }
    }
}

Si vous voyez quelque chose de mal ici, s'il vous plaît faites le moi savoir.

7
csharptest.net

Je ne vais pas vous écrire une solution exhaustive qui réponde à toutes vos exigences, mais je vais vous offrir une perspective. Mais dans l’ensemble, je pense que vous visez ces objectifs avec la lune.

L'architecture Invoke/BeginInvoke exécute simplement un délégué fourni sur le thread d'interface utilisateur du contrôle en lui envoyant un message Windows et la boucle de message exécute elle-même le délégué. Le fonctionnement spécifique de cette opération est sans importance, mais le fait est qu'il n'y a aucune raison particulière pour que vous deviez utiliser cette architecture pour la synchronisation de threads avec le thread d'interface utilisateur. Tout ce dont vous avez besoin, c'est d'une autre boucle en cours d'exécution, telle que dans un Forms.Timer ou quelque chose du genre, qui surveille une Queue pour que les délégués s'exécutent. Ce serait assez simple de mettre en œuvre le vôtre, bien que je ne sache pas exactement ce que cela vous apporterait que Invoke et BeginInvoke ne fournissent pas.

2
Adam Robinson

Wow, longue question. Je vais essayer d’organiser ma réponse afin que vous puissiez me corriger si je comprends quelque chose de mal, ok?

1) Si vous avez une très bonne raison d'appeler des méthodes d'interface utilisateur directement à partir de différents threads, ne le faites pas. Vous pouvez toujours choisir le modèle producteur/consommateur à l'aide de gestionnaires d'événements:

protected override void OnLoad()
{
    //...
    component.Event += new EventHandler(myHandler);
}

protected override void OnClosing()
{
    //...
    component.Event -= new EventHandler(myHandler);
}

par exemple, myHandler est déclenché à chaque fois que le composant d'un thread différent doit exécuter quelque chose dans l'interface utilisateur. En outre, la configuration du gestionnaire d'événements dans OnLoad et la désinscription dans OnClosing permettent de s'assurer que les événements ne seront reçus/traités que par l'interface utilisateur pendant que son descripteur est créé et prêt à traiter les événements. Vous ne pourrez même pas déclencher des événements dans cette boîte de dialogue si celle-ci est en cours de suppression, car vous ne serez plus abonné à l'événement. Si un autre événement est déclenché alors qu'il est toujours en cours de traitement, il sera mis en file d'attente.

Vous pouvez transmettre toutes les informations dont vous avez besoin dans les arguments d'événement: que vous mettiez à jour l'avancement, fermiez la fenêtre, etc.

2) Vous n'avez pas besoin d'InvokeRequired si vous utilisez le modèle que j'ai suggéré ci-dessus. Dans cet exemple, vous savez que la seule chose qui déclenche myHandler sera votre composant qui réside dans un autre thread, par exemple.

private void myHandler(object sender, EventArgs args)
{
    BeginInvoke(Action(myMethod));
}

Vous pouvez donc toujours utiliser invoke pour vous assurer que vous êtes dans le bon fil.

3) Faites attention aux appels synchrones. Si vous le souhaitez, vous pouvez remplacer Invoke au lieu de BeginInvoke. Cela bloquera votre composant jusqu'à ce que l'événement soit traité. Toutefois, si dans l'interface utilisateur, vous devez communiquer avec un élément exclusif du fil dans lequel réside votre composant, vous pouvez rencontrer des problèmes d'interblocage. (Je ne sais pas si je me suis bien fait comprendre, s'il vous plaît faites le moi savoir). J'ai eu des problèmes avec des exceptions lors de l'utilisation de réflexion (TargetInvocationException) et BeginInvoke (car ils démarrent un autre thread, vous perdez une partie de la trace de la pile), mais je ne me souviens pas d'avoir eu beaucoup de problèmes avec les appels Invoke, vous devriez donc soyez prudent quand il s'agit d'exceptions.

Whoa, longue réponse. Si, par hasard, je manquais à l'une de vos exigences ou que je comprenais mal quelque chose que vous aviez dit (l'anglais n'est pas ma langue maternelle, nous ne sommes jamais sûrs de cela), veuillez me le faire savoir.

1
diogoriba

C'est une question assez difficile. Comme je l'ai mentionné dans un commentaire, je ne pense pas que ce soit résoluable étant donné les contraintes documentées. Vous pouvez le pirater avec une implémentation particulière du framework .net: connaître l'implémentation de différentes fonctions membres peut vous aider à tricher en saisissant des verrous ici et là et en sachant que "ça va OK, appeler d'autres fonctions membres sur un autre thread. "

Donc, ma réponse de base pour l'instant est "non". Je déteste dire que ce n’est pas possible parce que j’ai beaucoup confiance dans le cadre .Net. Aussi, je suis comparativement novice, n'ayant pas étudié de framework en général, ni de CS, mais Internet est ouvert (même aux ignorants comme moi)!

Sur un sujet différent, l'argument peut être posé et bien pris en charge: "Vous ne devriez jamais avoir besoin de Invoke, utilisez uniquement BeginInvoke, et tirez et oubliez." Je ne prendrai pas la peine d'essayer de l'appuyer ou même de dire que c'est une affirmation correcte, mais je dirai que la mise en œuvre commune est incorrecte et qu'elle en posera une de travail (j'espère).

Voici une implémentation commune (tirée d'une réponse différente ici):

protected override void OnLoad()
{
    //...
    component.Event += new EventHandler(myHandler);
}

protected override void OnClosing()
{
    //...
    component.Event -= new EventHandler(myHandler);
}

Ce n'est pas thread-safe. Le composant aurait pu facilement commencer à appeler la liste d’invocations juste avant la désinscription, et ce n’est que lorsque nous aurons fini de disposer du gestionnaire que celui-ci sera invoqué. Le vrai problème est que rien ne montre comment chaque composant doit utilise le mécanisme d'événement en .Net, et honnêtement, il n'a pas à vous désabonner du tout: une fois que vous avez donné votre numéro de téléphone, personne n'est requis. pour l'effacer!

Mieux vaut:

protected override void OnLoad(System.EventArgs e)
{
    component.Event += new System.EventHandler(myHandler);
}    
protected override void OnFormClosing(FormClosedEventArgs e)
{
    component.Event -= new System.EventHandler(myHandler);
    lock (lockobj)
    {
        closing = true;
    }
}
private void Handler(object a, System.EventArgs e)
{
    lock (lockobj)
    {
        if (closing)
            return;
        this.BeginInvoke(new System.Action(HandlerImpl));
    }
}
/*Must be called only on GUI thread*/
private void HandlerImpl()
{
    this.Hide();
}
private readonly object lockobj = new object();
private volatile bool closing = false;

S'il vous plaît laissez-moi savoir si j'ai manqué quelque chose.

1
Limited Atonement

J'essaie d'organiser tous les messages d'appel de ce type sur l'interface graphique sous forme de feu et d'oubli (gestion de l'exception que l'interface graphique peut déclencher en raison de la situation de concurrence critique lors de l'élimination du formulaire).

De cette façon, s'il ne s'exécute jamais, aucun mal n'est fait.

Si l'interface graphique doit répondre au fil de travail, elle dispose d'un moyen d'inverser efficacement la notification. Pour des besoins simples, BackgroundWorker gère déjà cela.

1
Chris Chilvers

Ce n'est pas vraiment une réponse à la deuxième partie de la question, mais je l'inclurai juste pour la référence:

private delegate object SafeInvokeCallback(Control control, Delegate method, params object[] parameters);
public static object SafeInvoke(this Control control, Delegate method, params object[] parameters)
{
    if (control == null)
        throw new ArgumentNullException("control");
    if (control.InvokeRequired)
    {
        IAsyncResult result = null;
        try { result = control.BeginInvoke(new SafeInvokeCallback(SafeInvoke), control, method, parameters); }
        catch (InvalidOperationException) { /* This control has not been created or was already (more likely) closed. */ }
        if (result != null)
            return control.EndInvoke(result);
    }
    else
    {
        if (!control.IsDisposed)
            return method.DynamicInvoke(parameters);
    }
    return null;
}

Ce code devrait éviter les pièges les plus courants avec Invoke/BeginInvoke et il est facile à utiliser. Il suffit de tourner

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

dans

control.SafeInvoke(...)

Une construction similaire est possible pour BeginInvoke.

1
Filip Navara

Voici ce que j'utilise actuellement. Il est basé sur l'utilisation de SynchronizationContext et a été inspiré par l'article du blog de JaredPar - voir sa réponse ci-dessus. Cela peut ne pas être parfait, mais cela évite certains des problèmes du PO que je connaissais aussi.

   // Homemade Action-style delegates to provide .Net 2.0 compatibility, since .Net 2.0 does not 
   //  include a non-generic Action delegate nor Action delegates with more than one generic type 
   //  parameter. (The DMethodWithOneParameter<T> definition is not needed, could be Action<T> 
   //  instead, but is defined for consistency.) Some interesting observations can be found here:
   //  http://geekswithblogs.net/BlackRabbitCoder/archive/2011/11/03/c.net-little-wonders-the-generic-action-delegates.aspx
   public delegate void DMethodWithNoParameters();
   public delegate void DMethodWithOneParameter<T>(T parameter1);
   public delegate void DMethodWithTwoParameters<T1, T2>(T1 parameter1, T2 parameter2);
   public delegate void DMethodWithThreeParameters<T1, T2, T3>(T1 parameter1, T2 parameter2, T3 parameter3);


   /// <summary>
   /// Class containing support code to use the SynchronizationContext mechanism to dispatch the 
   /// execution of a method to the WinForms UI thread, from another thread. This can be used as an 
   /// alternative to the Control.BeginInvoke() mechanism which can be problematic under certain 
   /// conditions. See for example the discussion here:
   /// http://stackoverflow.com/questions/1364116/avoiding-the-woes-of-invoke-begininvoke-in-cross-thread-winform-event-handling
   ///
   /// As currently coded this works with methods that take zero, one, two or three arguments, but 
   /// it is a trivial job to extend the code for methods taking more arguments.
   /// </summary>
   public class WinFormsHelper
   {
      // An arbitrary WinForms control associated with thread 1, used to check that thread-switching 
      //  with the SynchronizationContext mechanism should be OK
      private readonly Control _thread1Control = null;

      // SynchronizationContext for the WinForms environment's UI thread
      private readonly WindowsFormsSynchronizationContext _synchronizationContext;


      /// <summary>
      /// Constructor. This must be called on the WinForms UI thread, typically thread 1. (Unless 
      /// running under the Visual Studio debugger, then the thread number is arbitrary.)
      ///
      /// The provided "thread 1 control" must be some WinForms control that will remain in 
      /// existence for as long as this object is going to be used, for example the main Form 
      /// control for the application.
      /// </summary>
      /// <param name="thread1Control">see above</param>
      public WinFormsHelper(Control thread1Control)
      {
         _thread1Control = thread1Control;
         if (thread1Control.InvokeRequired)
            throw new Exception("Not called on thread associated with WinForms controls.");

         _synchronizationContext =
                            SynchronizationContext.Current as WindowsFormsSynchronizationContext;
         if (_synchronizationContext == null) // Should not be possible?
            throw new Exception("SynchronizationContext.Current = null or wrong type.");
      }


      // The following BeginInvoke() methods follow a boilerplate pattern for how these methods 
      // should be implemented - they differ only in the number of arguments that the caller wants 
      // to provide.

      public void BeginInvoke(DMethodWithNoParameters methodWithNoParameters)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithNoParameters();
         }, null);
      }


      public void BeginInvoke<T>(DMethodWithOneParameter<T> methodWithOneParameter, T parameter1)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithOneParameter(parameter1);
         }, null);
      }


      public void BeginInvoke<T1, T2>(DMethodWithTwoParameters<T1, T2> methodWithTwoParameters,
                                      T1 parameter1, T2 parameter2)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithTwoParameters(parameter1, parameter2);
         }, null);
      }


      public void BeginInvoke<T1, T2, T3>(DMethodWithThreeParameters<T1, T2, T3> methodWithThreeParameters,
                                          T1 parameter1, T2 parameter2, T3 parameter3)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithThreeParameters(parameter1, parameter2, parameter3);
         }, null);
      }
   }
0
RenniePet

Pourquoi ne pas simplement masquer la boîte de dialogue lorsque l'utilisateur la rejette? Cela devrait fonctionner correctement si vous ne montrez pas ce dialogue modalement. (utilisez show au lieu de showdialog). Je crois que vous pouvez conserver votre boîte de dialogue de progression au-dessus de votre propre fenêtre (si vous en avez besoin) en transmettant l'hôte à la boîte de dialogue lorsque vous appelez show.

0
JMarsch

Utiliser System.ComponentModel.ISynchronizeInvoke est agréable lors de la création d'un System.ComponentModel.Component, tel que BackgroundWorker. L'extrait de code suivant décrit comment la variable FileSystemWater gère les événements.

    ''' <summary>
    ''' Gets or sets the object used to marshal the event handler calls issued as a result of finding a file in a search.
    ''' </summary>
    <IODescription(SR.FSS_SynchronizingObject), DefaultValue(CType(Nothing, String))> _
    Public Property SynchronizingObject() As System.ComponentModel.ISynchronizeInvoke
        Get
            If (_synchronizingObject Is Nothing) AndAlso (MyBase.DesignMode) Then
                Dim oHost As IDesignerHost = DirectCast(MyBase.GetService(GetType(IDesignerHost)), IDesignerHost)
                If (Not (oHost Is Nothing)) Then
                    Dim oRootComponent As Object = oHost.RootComponent
                    If (Not (oRootComponent Is Nothing)) AndAlso (TypeOf oRootComponent Is ISynchronizeInvoke) Then
                        _synchronizingObject = DirectCast(oRootComponent, ISynchronizeInvoke)
                    End If
                End If
            End If
            Return _synchronizingObject
        End Get
        Set(ByVal Value As System.ComponentModel.ISynchronizeInvoke)
            _synchronizingObject = Value
        End Set
    End Property

    Private _onStartupHandler As EventHandler

    Protected Sub OnStartup(ByVal e As EventArgs)
        If ((Not Me.SynchronizingObject Is Nothing) AndAlso Me.SynchronizingObject.InvokeRequired) Then
            Me.SynchronizingObject.BeginInvoke(_onStartupHandler, New Object() {Me, e})
        Else
            _onStartupHandler.Invoke(Me, e)
        End If
    End Sub
0
AMissico

Si vous n'aimez pas le BackgroundWoker (comme décrit par @Pavel), vous pouvez consulter cette bibliothèque http://www.wintellect.com/PowerThreading.aspx .

0
Kane

Si je comprends cela, pourquoi avez-vous besoin de supprimer la boîte de dialogue de progression pendant l'exécution de l'application? Pourquoi ne pas simplement afficher et masquer à la demande des utilisateurs? Cela semble rendre votre problème au moins un peu plus simple.

0
alexD