Laden...

Threadübergreifender Eventmechanismus (AsnycOperation)

Erstellt von parsifal vor 15 Jahren Letzter Beitrag vor 15 Jahren 4.405 Views
P
parsifal Themenstarter:in
66 Beiträge seit 2006
vor 15 Jahren
Threadübergreifender Eventmechanismus (AsnycOperation)

Beschreibung:

Der folgende Codeauszug soll verdeutlichen wie der Einsatz der Klasse AsyncOperation genutzt werden kann, um Controls im UI-Thread, durch ein Ereignis das in einem anderen Thread ausgelöst wurde, zu aktualisieren ohne auf delegates und die Abfrage der Eigenschaft InvokeRequired zurückzugreifen.

Ausgangsbasis ist eine Klasse 'EventService' die Ereignisse anbietet um über Änderungen an Daten zu informieren. Diese ist durch die Schnittstelle 'IEventService' definiert ist. Eine Instanz der Klasse 'EventService' befindet sich in einem anderen Thread (oder Prozess), als dem UI-Thread und bietet Ereignisse an, die z. B. dazu verwendet werden Controls im UI-Thread zu aktualisieren.


public interface IEventService {
  event EventHandler Changed;
}

public class EventService : IEventService {
  #region IEventService Members

  public event EventHandler Changed;

  #endregion

  public void ChangeSomething() {
    // Ändert irgendetwas und feuert dann das Ereignis Changed.
    ThrowChangedEvent( EventArgs.Empty );
  }

  protected virtual void ThrowChangedEvent( EventArgs e ) {
    EventHandler changed = Changed;
    if ( changed != null ) {
      changed( this, e );
    }
  }
}

Im o. g. Szenario würde man eine InvalidOperationException mit dem u. g. Text erhalten, wenn man z. B. versuchen würde in der EventHandler-Methode eine Textbox mit Text zu füllen.


Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement Form1
erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde.

Um diese threadübergreifenden Vorgänge zu umgehen, ist in der EventHandler-Methode die Eigenschaft InvokeRequired von Controls zu prüfen,
ob ein direkter oder ein indirekter Zugriff (mittels delegate und Invoke) erfolgen kann/muss.


private void OnEventServiceChanged( object sender, EventArgs e ) {
  if ( this.InvokeRequired ) {
    this.Invoke( new MethodInvoker( DisplayText ) );
  }
  else {
    DisplayText();
  }
}

private void DisplayText() {
  textBox1.AppendText( "Es hat sich was getan" );
  textBox1.AppendText( Environment.NewLine );
}

Eine andere Möglichkeit, diese threadübergreifenden Vorgänge zu ermöglichen ohne die Eigenschaft InvokeRequired zu prüfen und der Definition/Implementierung von Delegates, die für den Invoke benötigt werden, besteht darin eine Wrapper für die Klasse 'EventService' zu implementieren, der ebenfalls die Schnittstelle 'IEventService' implementiert.

Der Wrapper erhält eine Referenz auf den echten 'EventService' und registriert beim diesem alle angebotenen Ereignisse. Intern erstellt der Wrapper im Konstruktor eine AsyncOperation über den AsyncOperationManager, die er zum Auslösen der eigenen Ereignisse benötigt. Löst nun der echte 'EventService' ein Ereignis aus, wird die EventHandler-Methode des Wrappers aufgerufen. In dieser löst der Wrapper über den Aufruf Post(System.Threading.SendOrPostCallback, object) das entsprechende eigene Ereignis aus.

Dabei wird das Ereignisse des Wrappers in dem Thread ausgelöst, in dem er erstellt wurde. Wenn also der Wrapper im UI-Thread erzeugt wird. z. B innerhalb einer Form, dann werden die Ereignisse des Wrappers im UI-Thread ausgeführt und es kann ein direkter Zugriff auf die Controls erfolgen.

Nachfolgend ist die Implementierung der Wrapper-Klasse für den oben angegebenen 'EventService' dargestellt.


public class WrappedEventService : IEventService {

  private IEventService _realEventService;
  private AsyncOperation _operation;

  public WrappedEventService() {
    _operation = AsyncOperationManager.CreateOperation( Guid.NewGuid() );
  }

  public WrappedEventService( IEventService realEventService ) : this() {
	RealEventService = realEventService;
  }

  public IEventService RealEventService {
    get { return _realEventService; }
    set {
      if ( _realEventService != null ) {
        _realEventService.Changed -= OnChanged;
      }
      _realEventService = value;
      if ( _realEventService != null ) {
        _realEventService.Changed += OnChanged;
      }
    }
  }

  private void OnChanged( object sender, EventArgs e ) {
    _operation.Post( ThrowChangedEvent, e );
  }

  protected virtual void ThrowChangedEvent( object e ) {
    EventHandler changed = Changed;
    if ( changed != null ) {
      changed( this, ( EventArgs ) e );
    }
  }

  #region IEventService Members

  public event EventHandler Changed;

  #endregion
}

In der angehängten Zip habe ich ein Beispielprogramm (Visual Studio 2008 Solution) hinterlegt, um den Sachverhalt praktisch zu verdeutlichen. Der wesentliche Code ist im Konstruktor der Klasse Form1 enthalten. Die wesentlichen Stellen sind mit Kommentaren versehen.

Events, InvokeRequired, AsyncOperation, UI-Thread, Threadübergreifend

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo parsifal,

mit scheint es vor dem Hintergrund, dass es es nur einen GUI-Thread geben sollte, etwas viel Aufwand dafür zu sein, weil es ja reichen würde, die AsnycOperation im GUI-Thread zu erzeugen und an den EventService zu übergeben (z.B. über eine entsprechende Property), so dass schon dieser EventService AsnycOperation.Post verwenden kann, ohne dass man einen Wrapper braucht.

Davon abgesehen, entspricht die Namensgebung bei den Events nicht dem Empfehlungen von Microsoft, nach dem EventService.ThrowChangedEvent EventService.OnChanged heißen müsste. Für ChangeSomething gibt es zwar wohl keine offiziellen Vorgaben, aber ich würde es wohl eher FireChanged nennen.

WrappedEventService.OnChanged müsste WrappedEventService-EventService_Changed heißen und WrappedEventService.ThrowChangedEvent WrappedEventService.OnChanged.

herbivore

L
254 Beiträge seit 2005
vor 15 Jahren

"In general, you should provide a protected method called OnXxx on types with events that can be overridden in a derived class. This method should only have the event parameter e, because the sender is always the instance of the type."

http://msdn.microsoft.com/en-us/library/h0eyck3s(VS.71).aspx

Grundsätzlich heisst ein Event das gegen aussen verwendet werden kann immer so wie das was es logisch ist.

Also "List.ItemRemoved"

Intern ist es richtig beim remove eine "FireRemovedEvent" Methode zu nennen, auch wenn im Public API nicht interessiert (eigentlich kann der das intern nennen wie er will).

Das ItemRemoved, würde dann eine protected "OnRemove" Methode aufrufen die logischerweise in der ableitenden Klasse überschrieben werden könnte.

Das einzige ungewöhnliche finde ich ist das "FireChangedEvent" Public ist, aber das würde sich dann auflösen wenn mans ein wenig umbaut.

"Für ChangeSomething gibt es zwar wohl keine offiziellen Vorgaben, aber ich würde es wohl eher FireChanged nennen"

Find ich jetzt aber ganz schlecht, da das eigentlich nicht das gleiche ist. Die Methode macht etwas, was ändert und so ein Event schmeisst. Diese Methode in "ThrowEvent" umzubennenne finde ich dann aber irgendwie ganz komisch?

Gruss

If you can't make it, fake it.

5.299 Beiträge seit 2008
vor 15 Jahren

Hi!

Grade habich meine [ExtensionMethods]CrossThread-Calls runderneuert. Damit könntest du ohne weitere Umstände aus einem Nebenthread heraus ein Event im Gui-Thread raisen:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1 {
   public class EventService2 {

      public event EventHandler Changed;

      /// <summary>
      /// Führt Änderungen an Daten durch und feuert anschließend das Ereignis <see cref="Changed"/>.
      /// </summary>
      public void ChangeSomething() {
         CrossThread.RunGui(ThrowChangedEvent, EventArgs.Empty);
      }

      /// <summary>
      /// Wirft das Ereignis <see cref="Changed"/>.
      /// </summary>
      protected virtual void ThrowChangedEvent(EventArgs e) {
         EventHandler changed = Changed;
         if (changed != null) {
            changed(this, e);
         }
      }
   }
}

Der frühe Apfel fängt den Wurm.