2009-12-22 8 views
11

Ich verwende einen SynchronizationContext, um Ereignisse zurück zum Benutzeroberflächenthread von meiner DLL zu mappen, die viele Multithread-Hintergrundaufgaben ausführt.Verwenden von SynchronizationContext zum Senden von Ereignissen zurück an die Benutzeroberfläche für WinForms oder WPF

Ich weiß, dass das Singleton-Muster kein Favorit ist, aber ich benutze es jetzt, um eine Referenz des UI-SynchronizationContext zu speichern, wenn Sie foo's Elternobjekt erstellen.

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

    } 
} 

Dies überhaupt in WPF nicht funktioniert hat, der TheUISync Instanzen UI-Sync (die aus dem Hauptfenster füttern wird) paßt nie den aktuellen SynchronizationContext.Current. In Windows-Form, wenn ich dasselbe mache, werden sie nach einem Aufruf übereinstimmen und wir werden zum richtigen Thread zurückkehren.

Mein fix, was ich hasse, sieht aus wie

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

    } 
} 

Ich hoffe also, diese Probe genug Sinn macht, zu folgen.

Antwort

37

Das unmittelbare Problem

Ihr unmittelbares Problem ist, dass SynchronizationContext.Current nicht automatisch für WPF gesetzt. So legen Sie es brauchen Sie so etwas wie dies in Ihrem TheUISync Code tun, wenn sie unter WPF ausgeführt wird:

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

ein tieferes Problem

SynchronizationContext wird mit dem COM + Unterstützung gebunden in und ist so konzipiert, Fäden überqueren . In WPF kann kein Dispatcher verwendet werden, der sich über mehrere Threads erstreckt, so dass eine SynchronizationContext Threads nicht wirklich kreuzen kann. Es gibt eine Reihe von Szenarien, in denen ein SynchronizationContext zu einem neuen Thread wechseln kann - speziell alles, was ExecutionContext.Run() aufruft. Wenn Sie also SynchronizationContext verwenden, um sowohl WinForms- als auch WPF-Clients Ereignisse bereitzustellen, müssen Sie beachten, dass einige Szenarien fehlschlagen, beispielsweise eine Webanforderung an einen Webdienst oder eine im selben Prozess gehostete Website.

Wie SynchronizationContext dies umgehen, benötigen

Weil ich vorschlagen, mit ausschließlich Dispatcher Mechanismus WPF zu diesem Zweck auch mit Code WinForms. Sie haben eine Singleton-Klasse "TheUISync" erstellt, in der die Synchronisation gespeichert ist. Sie haben also eine Möglichkeit, sich in die oberste Ebene der Anwendung einzuklinken. Wie auch immer Sie das tun, Sie können Code hinzufügen, der etwas WPF-Inhalt zu Ihrer WinForms-Anwendung hinzufügt, so dass funktioniert, dann verwenden Sie den neuen -Mechanismus, den ich unten beschreibe.

Dispatcher anstelle von SynchronizationContext

Dispatcher Mechanismus WPF beseitigt tatsächlich die Notwendigkeit eines separaten SynchronizationContext Objekt.Sofern Sie nicht bestimmte Interop-Szenarien wie das Teilen von Code mit COM + -Objekten oder WinForms-Benutzeroberflächen haben, verwenden Sie am besten statt SynchronizationContext.

Das sieht aus wie:

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

Beachten Sie, dass Sie nicht ein Objekt TheUISync länger brauchen - WPF Griffe für Sie, dass Detail.

Wenn Sie sich wohler mit der älteren delegate Syntax sind, können Sie es auf diese Weise tun statt:

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

Ein unabhängiger Fehler

auch zu beheben beachten Sie, dass es einen Fehler in Ihr Originalcode, der hier repliziert wird. Das Problem ist, dass FooDoneEvent zwischen dem Zeitpunkt, zu dem OnFooDoDone aufgerufen wird, und dem Zeitpunkt, an dem der BeginInvoke (oder Post im ursprünglichen Code) den Delegaten aufruft, auf null gesetzt werden kann. Der Fix ist ein zweiter Test innerhalb des Delegierten:

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

Unabhängig davon, wann Sie überprüfen, es sei denn, Sie tatsächlich etwas sperren, ich denke, Sie könnten immer dieses Problem haben. Und das ist ein Threading-Problem, das nicht in Ihrer Klasse behoben werden kann, es muss in der Klasse behandelt werden, die Verkabelung in Ihrem Ereignis ist (oder von Ihrem Ereignis abmelden), obwohl ich ein Sperrobjekt bereitstellen müsste Teilnehmer könnte verwenden und ich könnte verwenden, um sicherzustellen, dass die Ereignisse syncrhornized sind. –

+1

Ja und nein. "public event ..." hat Probleme mit sich selbst, wenn Sie davon ausgehen, dass es sich um "free-threaded" handelt. Wenn zwei Threads beide dasselbe Ereignis zur gleichen Zeit abonnieren, können sie möglicherweise nicht beide in die Aufrufliste gelangen. Daher ist die Annahme immer, dass Sie ein Threading-Protokoll im Hinterkopf haben, um das Ereignis an erster Stelle zu setzen. Das gebräuchlichste Protokoll besteht darin, solche Ereignisse nur für den Benutzeroberflächenthread festzulegen. In diesem Fall funktioniert die Fehlerbehebung, die ich angegeben habe, zuverlässig, während der ursprüngliche Code dies nicht tut. Wenn andererseits ein anderes Protokoll beabsichtigt ist, müssen Sie möglicherweise explizit gesperrt werden. –

+2

Um sicherzustellen, dass 'FooDoDoneEvent' nicht NULL ist, glaube ich, dass Sie wirklich' FooDoDoneEvent' einer lokalen Variablen zuweisen möchten, überprüfen Sie, dass es nicht null ist, und rufen Sie 'FooDoDoneEvent (this, new EventArgs()) auf. '. – Charles

0

Anstatt mit dem aktuellen zu vergleichen, warum nicht einfach lassen es sich sorgen machen; dann ist es einfach ein Fall von Umgang mit dem „Kontext“ Falles:

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

Ich vergesse, warum dies fehlschlägt, aber ich weiß, dass ich das schon versucht habe. Ich kann mich nicht erinnern, ob es Winform Test App war, die Unit-Tests oder die WPF-App, die brach, aber diese Lösung war inkompatibel mit mindestens einem dieser Techniker. –

+3

Dieser Code funktioniert nicht, da SynchronizationContext.Current im UI-Thread "erfasst" sein sollte, während RaiseOnUIThread in jedem anderen Thread aufgerufen werden kann. –

Verwandte Themen