2010-08-25 5 views
5

Le code suivant illustre mon dilemme. Le code crée un thread d'arrière-plan qui traite quelque chose, puis appelle le thread d'interface utilisateur avec le résultat.C# Winforms Threading: Formulaire fermé Obtient

Il peut lever une exception si le thread d'arrière-plan appelle Invoke sur le formulaire après la fermeture du formulaire. Il vérifie IsHandleCreated avant d'appeler Invoke, mais le formulaire peut se fermer après la vérification. Une solution peut être de synchroniser FormClosing et chaque appel à Invoke, mais cela ne semble pas très élégant. Y a-t-il un moyen plus facile?

Répondre

4

Oui, il y a une course ici. A prend une bonne milliseconde avant que la cible commence à fonctionner. Cela fonctionnera "mieux" si vous utilisez Control.BeginInvoke() à la place, l'implémentation Dispose() du formulaire vide la file d'attente de répartition. Mais c'est encore une course, même si elle frappera très rarement. Votre code tel qu'il est écrit dans l'extrait n'exige pas Invoke().

La seule solution de nettoyage consiste à interverrouiller l'événement FormClosing et à de retarder jusqu'à ce que vous obteniez la confirmation que le thread d'arrière-plan est terminé et ne peut pas être redémarré. Pas facile à faire avec votre code tel quel car cela nécessite un rappel 'terminé' pour que vous puissiez vraiment fermer le formulaire. BackgroundWorker would be a better mousetrap. Le correctif Q & D est d'attraper ObjectDisposedException que BeginInvoke va déclencher. Étant donné que cela sera rare lorsque vous utiliserez BeginInvoke(), ce hack laid pourrait être acceptable. Vous ne pouvez pas le tester :)

+0

Cela peut au moins l'atténuer. Mon code tel qu'il est écrit ne nécessite pas Invoke? Ces exceptions sont lancées environ 1 fois sur 10. Je peux les tester! :) – drifter

+0

Ce n'est pas le cas, pas besoin de retarder le thread et attendre que le délégué a fini de fonctionner. Vous ne faites rien * après * l'appel Invoke. Vous ne pouvez pas vraiment le tester une fois que vous utilisez BeginInvoke, vous devez ouvrir et fermer votre formulaire au moins un million de fois. Soyez assuré que la course est toujours là, vous aurez besoin d'attraper ODE. –

+0

Cela ne me dérangerait pas d'attraper ObjectDisposedException, mais parfois il lance InvalidOperationException à la place ("Invoke ou BeginInvoke ne peut pas être appelé sur un contrôle tant que le handle de fenêtre n'a pas été créé.") Je ne peux pas l'attraper InvalidOperationException est dans le code invoqué, n'est-ce pas? – drifter

0

Vous pouvez vérifier IsDisposed sur le formulaire (ou tout autre contrôle) avant de l'appeler.

Vous devriez également vérifier ceci dans la méthode que vous appelez, dans le cas où le formulaire a été jeté entre-temps.

+1

Le problème est que, dans certains endroits Invoke lance ObjectDisposedException même s'il vérifie IsDisposed immédiatement avant l'appel. – drifter

1

Jetez un oeil à WindowsFormsSynchronizationContext. Les messages de méthode Post appellent votre délégué UpdateUI sur le thread d'interface utilisateur sans avoir besoin d'une fenêtre dédiée; cela vous permet d'ignorer l'appel IsHandleCreated et Invoke.

Modifier: MSDN a quelques exemples de code sous "Multithreaded Programming with the Event-based Asynchronous Pattern".

Vous pourriez trouver plus facile de programmer via la classe AsyncOperationManager, qui se trouve au-dessus de WindowsFormsSynchronizationContext. À son tour, le composant BackgroundWorker est construit au-dessus de AsyncOperationManager.

Le thread d'interface utilisateur est défini comme celui sur lequel vous appelez AsyncOperationManager.CreateOperation; vous voulez appeler CreateOperation au début de MyMethod, lorsque vous savez que vous êtes sur le thread d'interface utilisateur et capturer sa valeur de retour dans une variable locale.

+0

J'aime beaucoup ça. Certains de mes utilisateurs sont têtus à mettre à jour vers .NET 4, mais une affaire monte en sa faveur. – drifter

+0

C'est une classe .NET 2.0. Ne résout pas le problème, Control.Invoke l'utilise déjà. Post() lancera ObjectDisposedException. –

+0

Mais 'WindowsFormsSynchronizationContext' publie via un contrôle caché qui est fermé après la fermeture de votre formulaire et juste avant la fermeture du programme. Votre formulaire n'est pas impliqué du tout. –

2

J'ai résolu ce problème de synchronisation pour BeginInvoke en utilisant la recommandation de Hans Passant d'attraper ObjectDisposedException. Jusqu'à présent, cela semble fonctionner. J'ai créé des méthodes d'extension de la classe Control pour faciliter cela. TryBeginInvoke tente d'invoquer sa propre méthode sur le contrôle

Si la méthode est appelée avec succès, elle vérifie si le contrôle a été éliminé. Si elle a été éliminée, elle revient immédiatement; sinon, il appelle la méthode initialement transmise en tant que paramètre à TryBeginInvoke.Le code est le suivant:

public static class ControlExtension 
{ 
    // --- Static Fields --- 
    static bool _fieldsInitialized = false; 
    static InvokeDelegateDelegate _methodInvokeDelegate; // Initialized lazily to reduce application startup overhead [see method: InitStaticFields] 
    static InvokeMethodDelegate _methodInvokeMethod; // Initialized lazily to reduce application startup overhead [see method: InitStaticFields] 

    // --- Public Static Methods --- 
    public static bool TryBeginInvoke(this Control control, Delegate method, params object[] args) 
    { 
     IAsyncResult asyncResult; 
     return TryBeginInvoke(control, method, out asyncResult, args); 
    } 

    /// <remarks>May return true even if the target of the invocation cannot execute due to being disposed during invocation.</remarks> 
    public static bool TryBeginInvoke(this Control control, Delegate method, out IAsyncResult asyncResult, params object[] args) 
    { 
     if (!_fieldsInitialized) 
      InitStaticFields(); 

     asyncResult = null; 

     if (!control.IsHandleCreated || control.IsDisposed) 
      return false; 

     try 
     { 
      control.BeginInvoke(_methodInvokeDelegate, control, method, args); 
     } 
     catch (ObjectDisposedException) 
     { 
      return false; 
     } 
     catch (InvalidOperationException) // Handle not created 
     { 
      return false; 
     } 

     return true; 
    } 

    public static bool TryBeginInvoke(this Control control, MethodInvoker method) 
    { 
     IAsyncResult asyncResult; 
     return TryBeginInvoke(control, method, out asyncResult); 
    } 

    /// <remarks>May return true even if the target of the invocation cannot execute due to being disposed during invocation.</remarks> 
    public static bool TryBeginInvoke(this Control control, MethodInvoker method, out IAsyncResult asyncResult) 
    { 
     if (!_fieldsInitialized) 
      InitStaticFields(); 

     asyncResult = null; 

     if (!control.IsHandleCreated || control.IsDisposed) 
      return false; 

     try 
     { 
      control.BeginInvoke(_methodInvokeMethod, control, method); 
     } 
     catch (ObjectDisposedException) 
     { 
      return false; 
     } 
     catch (InvalidOperationException) // Handle not created 
     { 
      return false; 
     } 

     return true; 
    } 

    // --- Private Static Methods --- 
    private static void InitStaticFields() 
    { 
     _methodInvokeDelegate = new InvokeDelegateDelegate(InvokeDelegate); 
     _methodInvokeMethod = new InvokeMethodDelegate(InvokeMethod); 
    } 
    private static object InvokeDelegate(Control control, Delegate method, object[] args) 
    { 
     if (!control.IsHandleCreated || control.IsDisposed) 
      return null; 

     return method.DynamicInvoke(args); 
    } 
    private static void InvokeMethod(Control control, MethodInvoker method) 
    { 
     if (!control.IsHandleCreated || control.IsDisposed) 
      return; 

     method(); 
    } 

    // --- Private Nested Types --- 
    delegate object InvokeDelegateDelegate(Control control, Delegate method, object[] args); 
    delegate void InvokeMethodDelegate(Control control, MethodInvoker method); 
}