2009-12-22 3 views
11

J'utilise un objet SynchronizationContext pour ramener les événements au thread d'interface utilisateur de ma DLL qui exécute de nombreuses tâches d'arrière-plan multithread. Je sais que le modèle singleton n'est pas un favori, mais je l'utilise pour maintenant stocker une référence de SynchronizationContext de l'interface utilisateur lorsque vous créez l'objet parent de foo.Utilisation de SynchronizationContext pour renvoyer des événements à l'interface utilisateur pour WinForms ou WPF

public class Foo 
{ 
    public event EventHandler FooDoDoneEvent; 

    public void DoFoo() 
    { 
     //stuff 
     OnFooDoDone(); 
    } 

    private void OnFooDoDone() 
    { 
     if (FooDoDoneEvent != null) 
     { 
      if (TheUISync.Instance.UISync != SynchronizationContext.Current) 
      { 
       TheUISync.Instance.UISync.Post(delegate { OnFooDoDone(); }, null); 
      } 
      else 
      { 
       FooDoDoneEvent(this, new EventArgs()); 
      } 
     } 

    } 
} 

Cela ne fonctionne pas du tout dans WPF, la synchronisation de l'interface utilisateur des instances TheUISync (qui soutire de la fenêtre principale) ne correspond jamais à la SynchronizationContext.Current actuelle. Sous forme de fenêtres, quand je fais la même chose, ils vont correspondre après une invocation et nous reviendrons au bon thread.

Mon correctif, que je déteste, ressemble

public class Foo 
{ 
    public event EventHandler FooDoDoneEvent; 

    public void DoFoo() 
    { 
     //stuff 
     OnFooDoDone(false); 
    } 

    private void OnFooDoDone(bool invoked) 
    { 
     if (FooDoDoneEvent != null) 
     { 
      if ((TheUISync.Instance.UISync != SynchronizationContext.Current) && (!invoked)) 
      { 
       TheUISync.Instance.UISync.Post(delegate { OnFooDoDone(true); }, null); 
      } 
      else 
      { 
       FooDoDoneEvent(this, new EventArgs()); 
      } 
     } 

    } 
} 

J'espère donc que cet échantillon fait assez de sens pour suivre.

Répondre

37

Le problème immédiat

Votre problème immédiat est que SynchronizationContext.Current n'est pas réglé automatiquement pour WPF. Pour le configurer, vous aurez besoin de faire quelque chose comme ceci dans votre code TheUISync lors de l'exécution sous WPF:

var context = new DispatcherSynchronizationContext(
        Application.Current.Dispatcher); 
SynchronizationContext.SetSynchronizationContext(context); 
UISync = context; 

Un problème plus profond

SynchronizationContext est lié avec le COM + support et est conçu pour traverser les discussions . Dans WPF, vous ne pouvez pas avoir un Dispatcher qui s'étend sur plusieurs threads, donc un SynchronizationContext ne peut pas réellement traverser les threads. Il existe un certain nombre de scénarios dans lesquels un SynchronizationContext peut passer à un nouveau thread - en particulier tout ce qui appelle ExecutionContext.Run(). Par conséquent, si vous utilisez SynchronizationContext pour fournir des événements à des clients WinForms et WPF, vous devez savoir que certains scénarios se briseront, par exemple une requête Web à un service Web ou un site hébergé dans le même processus poserait un problème.

Comment se déplacer besoin SynchronizationContext

En raison de cela, je suggère d'utiliser le mécanisme Dispatcher de WPF exclusivement à cet effet, même avec le code WinForms. Vous avez créé une classe singleton "TheUISync" qui stocke la synchronisation, de sorte que vous avez clairement un moyen de vous connecter au niveau supérieur de l'application. Quoi que vous fassiez, vous pouvez ajouter du code qui crée ajoute du contenu WPF à votre application WinForms afin que Dispatcher fonctionne, puis utilisez le nouveau mécanisme Dispatcher que je décris ci-dessous.

utilisant Dispatcher à la place d'un mécanisme SynchronizationContext

Dispatcher WPF élimine en fait la nécessité d'un objet SynchronizationContext séparé.À moins que vous n'ayez certains scénarios d'interopérabilité tels que le partage de code avec des objets COM + ou des interfaces utilisateur WinForms, votre meilleure solution est d'utiliser Dispatcher au lieu de SynchronizationContext.

Cela ressemble à:

public class Foo 
{ 
    public event EventHandler FooDoDoneEvent; 

    public void DoFoo() 
    { 
    //stuff 
    OnFooDoDone(); 
    } 

    private void OnFooDoDone() 
    { 
    if(FooDoDoneEvent!=null) 
     Application.Current.Dispatcher.BeginInvoke(
     DispatcherPriority.Normal, new Action(() => 
     { 
      FooDoDoneEvent(this, new EventArgs()); 
     })); 
    } 
} 

Notez que vous ne avez plus besoin d'un objet TheUISync - poignées WPF ce détail pour vous.

Si vous êtes plus à l'aise avec la syntaxe delegate plus vous pouvez le faire de cette façon à la place:

 Application.Current.Dispatcher.BeginInvoke(
     DispatcherPriority.Normal, new Action(delegate 
     { 
      FooDoDoneEvent(this, new EventArgs()); 
     })); 

Un bug sans lien de fixer

Notez également qu'il ya un bug dans votre code original qui est répliqué ici. Le problème est que FooDoneEvent peut être défini sur null entre le moment où OnFooDoDone est appelé et le moment où le BeginInvoke (ou Post dans le code d'origine) appelle le délégué. Le correctif est un deuxième test à l'intérieur du délégué:

if(FooDoDoneEvent!=null) 
     Application.Current.Dispatcher.BeginInvoke(
     DispatcherPriority.Normal, new Action(() => 
     { 
      if(FooDoDoneEvent!=null) 
      FooDoDoneEvent(this, new EventArgs()); 
     })); 
+0

Peu importe quand vous vérifiez, sauf si vous verrouillez quelque chose, je pense que vous pourriez toujours avoir ce problème. Et c'est un problème de threading qui ne peut pas être résolu dans votre classe, il doit être géré dans la classe qui est en train de se connecter à votre événement (ou de vous désabonner de votre événement). abonné pourrait utiliser et je pourrais utiliser pour s'assurer que les événements sont synchronisés. –

+1

Oui et non. "événement public ..." a des problèmes de threading si vous pensez qu'il peut être appelé thread-free. Si deux threads s'abonnent tous deux au même événement en même temps, ils peuvent tous deux ne pas entrer dans la liste d'invocation. Donc, l'hypothèse est toujours que vous avez un certain protocole de threading à l'esprit pour définir l'événement en premier lieu. Le plus commun de ces protocoles est de définir de tels événements uniquement sur le thread d'interface utilisateur, auquel cas le correctif de bogue que j'ai donné fonctionne de manière fiable alors que le code d'origine ne le fait pas. D'un autre côté, si un protocole différent est prévu, vous pouvez avoir besoin d'un verrouillage explicite. –

+2

Pour m'assurer que 'FooDoDoneEvent' n'est pas nul, je crois que ce que vous voulez vraiment faire est d'assigner' FooDoDoneEvent' à une variable locale, vérifiez qu'elle n'est pas nulle, puis appelez 'FooDoDoneEvent (this, new EventArgs()) '. – Charles

0

Plutôt que de comparer à l'actuel, pourquoi ne pas simplement laisser s'inquiéter à ce sujet; alors il est tout simplement un cas de manipulation du « pas de contexte » cas:

static void RaiseOnUIThread(EventHandler handler, object sender) { 
    if (handler != null) { 
     SynchronizationContext ctx = SynchronizationContext.Current; 
     if (ctx == null) { 
      handler(sender, EventArgs.Empty); 
     } else { 
      ctx.Post(delegate { handler(sender, EventArgs.Empty); }, null); 
     } 
    } 
} 
+0

J'oublie pourquoi cela échoue, mais je sais que j'ai déjà essayé. Je ne me souviens pas si c'était une application de test winform, les tests unitaires ou l'application WPF qui a cassé, mais cette solution était incompatible avec au moins une de ces technologies. –

+3

Ce code ne fonctionne pas car SynchronizationContext.Current doit être "capturé" sur le thread UI, alors que RaiseOnUIThread peut être appelé sur n'importe quel autre thread. –

Questions connexes