Sauberste Möglichkeit, Thread-übergreifende Ereignisse aufzurufen

78

Ich finde, dass das .NET-Ereignismodell so ist, dass ich häufig ein Ereignis in einem Thread auslöse und es in einem anderen Thread abhöre. Ich habe mich gefragt, wie ich ein Ereignis am saubersten von einem Hintergrund-Thread auf meinen UI-Thread übertragen kann.

Basierend auf den Community-Vorschlägen habe ich Folgendes verwendet:

// earlier in the code
mCoolObject.CoolEvent+= 
           new CoolObjectEventHandler(mCoolObject_CoolEvent);
// then
private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    if (InvokeRequired)
    {
        CoolObjectEventHandler cb =
            new CoolObjectEventHandler(
                mCoolObject_CoolEvent);
        Invoke(cb, new object[] { sender, args });
        return;
    }
    // do the dirty work of my method here
}
Nick
quelle
Beachten Sie, dass InvokeRequired möglicherweise false zurückgibt, wenn ein vorhandenes verwaltetes Steuerelement noch kein nicht verwaltetes Handle hat. Sie sollten bei Ereignissen, die ausgelöst werden, bevor die Kontrolle vollständig erstellt wurde, Vorsicht walten lassen.
GregC

Antworten:

27

Einige Beobachtungen:

  • Erstellen Sie einfache Delegaten nicht explizit in einem solchen Code, es sei denn, Sie sind vor 2.0, sodass Sie Folgendes verwenden können:
   BeginInvoke(new EventHandler<CoolObjectEventArgs>(mCoolObject_CoolEvent), 
               sender, 
               args);
  • Außerdem müssen Sie das Objektarray nicht erstellen und füllen, da der Parameter args vom Typ "params" ist, sodass Sie die Liste einfach übergeben können.

  • Ich würde wahrscheinlich begünstigen Invokeüber BeginInvokewie dieser führt in den Code asynchron kann , die aufgerufen wird , oder kann nicht sein , was Sie nach , aber würden nachfolgende Ausnahmen schwer ohne Aufruf zu verbreiten machen Handhabung EndInvoke. Was passieren würde, ist, dass Ihre App TargetInvocationExceptionstattdessen eine bekommt .

Shaun Austin
quelle
44

Ich habe einen Code dafür online. Es ist viel schöner als die anderen Vorschläge; Probieren Sie es auf jeden Fall aus.

Beispielnutzung:

private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    // You could use "() =>" in place of "delegate"; it's a style choice.
    this.Invoke(delegate
    {
        // Do the dirty work of my method here.
    });
}
Domenic
quelle
Sie können auch den Namespace System.Windows.Formsin Ihrer Erweiterung ändern . Auf diese Weise vermeiden Sie, dass Sie Ihren benutzerdefinierten Namespace jedes Mal hinzufügen, wenn Sie ihn benötigen.
Joe Almore
10

Ich meide überflüssige Delegiertenerklärungen.

private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    if (InvokeRequired)
    {
        Invoke(new Action<object, CoolObjectEventArgs>(mCoolObject_CoolEvent), sender, args);
        return;
    }
    // do the dirty work of my method here
}

Für Nicht-Ereignisse können Sie den System.Windows.Forms.MethodInvokerDelegaten oder verwenden System.Action.

BEARBEITEN: Zusätzlich hat jede Veranstaltung einen entsprechenden EventHandlerDelegierten, sodass es überhaupt nicht erforderlich ist, einen neu zu deklarieren.

Konrad Rudolph
quelle
1
Für mich hat es so funktioniert:Invoke(new Action<object, CoolObjectEventArgs>(mCoolObject_CoolEvent), sender, args);
António Almeida
@ ToniAlmeida Ja, das war ein Tippfehler in meinem Code. Vielen Dank für den Hinweis.
Konrad Rudolph
4

Ich habe die folgende "universelle" Cross-Thread-Aufrufklasse für meinen eigenen Zweck erstellt, aber ich denke, es lohnt sich, sie zu teilen:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

namespace CrossThreadCalls
{
  public static class clsCrossThreadCalls
  {
    private delegate void SetAnyPropertyCallBack(Control c, string Property, object Value);
    public static void SetAnyProperty(Control c, string Property, object Value)
    {
      if (c.GetType().GetProperty(Property) != null)
      {
        //The given property exists
        if (c.InvokeRequired)
        {
          SetAnyPropertyCallBack d = new SetAnyPropertyCallBack(SetAnyProperty);
          c.BeginInvoke(d, c, Property, Value);
        }
        else
        {
          c.GetType().GetProperty(Property).SetValue(c, Value, null);
        }
      }
    }

    private delegate void SetTextPropertyCallBack(Control c, string Value);
    public static void SetTextProperty(Control c, string Value)
    {
      if (c.InvokeRequired)
      {
        SetTextPropertyCallBack d = new SetTextPropertyCallBack(SetTextProperty);
        c.BeginInvoke(d, c, Value);
      }
      else
      {
        c.Text = Value;
      }
    }
  }

Und Sie können einfach SetAnyProperty () aus einem anderen Thread verwenden:

CrossThreadCalls.clsCrossThreadCalls.SetAnyProperty(lb_Speed, "Text", KvaserCanReader.GetSpeed.ToString());

In diesem Beispiel führt die obige KvaserCanReader-Klasse einen eigenen Thread aus und ruft auf, um die Texteigenschaft der Bezeichnung lb_Speed ​​im Hauptformular festzulegen.

TarPista
quelle
3

Ich denke, der sauberste Weg ist definitiv , die AOP-Route zu gehen. Nehmen Sie einige Aspekte vor, fügen Sie die erforderlichen Attribute hinzu, und Sie müssen die Thread-Affinität nie wieder überprüfen.

Dmitri Nesteruk
quelle
Ich verstehe Ihren Vorschlag nicht. C # ist keine nativ aspektorientierte Sprache. Denken Sie an ein Muster oder eine Bibliothek für die Implementierung von Aspekten, die das Marshalling hinter den Kulissen implementieren?
Eric
Ich verwende PostSharp, definiere also das Threading-Verhalten in einer Attributklasse und verwende dann beispielsweise das Attribut [WpfThread] vor jeder Methode, die im UI-Thread aufgerufen werden muss.
Dmitri Nesteruk
3

Verwenden Sie den Synchronisationskontext, wenn Sie ein Ergebnis an den UI-Thread senden möchten. Ich musste die Thread-Priorität ändern, also wechselte ich von der Verwendung von Thread-Pool-Threads (auskommentierter Code) und erstellte einen eigenen neuen Thread. Ich konnte weiterhin den Synchronisationskontext verwenden, um zurückzugeben, ob der Datenbankabbruch erfolgreich war oder nicht.

    #region SyncContextCancel

    private SynchronizationContext _syncContextCancel;

    /// <summary>
    /// Gets the synchronization context used for UI-related operations.
    /// </summary>
    /// <value>The synchronization context.</value>
    protected SynchronizationContext SyncContextCancel
    {
        get { return _syncContextCancel; }
    }

    #endregion //SyncContextCancel

    public void CancelCurrentDbCommand()
    {
        _syncContextCancel = SynchronizationContext.Current;

        //ThreadPool.QueueUserWorkItem(CancelWork, null);

        Thread worker = new Thread(new ThreadStart(CancelWork));
        worker.Priority = ThreadPriority.Highest;
        worker.Start();
    }

    SQLiteConnection _connection;
    private void CancelWork()//object state
    {
        bool success = false;

        try
        {
            if (_connection != null)
            {
                log.Debug("call cancel");
                _connection.Cancel();
                log.Debug("cancel complete");
                _connection.Close();
                log.Debug("close complete");
                success = true;
                log.Debug("long running query cancelled" + DateTime.Now.ToLongTimeString());
            }
        }
        catch (Exception ex)
        {
            log.Error(ex.Message, ex);
        }

        SyncContextCancel.Send(CancelCompleted, new object[] { success });
    }

    public void CancelCompleted(object state)
    {
        object[] args = (object[])state;
        bool success = (bool)args[0];

        if (success)
        {
            log.Debug("long running query cancelled" + DateTime.Now.ToLongTimeString());

        }
    }
Der einsame Kodierer
quelle
2

Ich habe mich immer gefragt, wie teuer es ist, immer davon auszugehen, dass ein Aufruf erforderlich ist ...

private void OnCoolEvent(CoolObjectEventArgs e)
{
  BeginInvoke((o,e) => /*do work here*/,this, e);
}

quelle
1
Durch Ausführen eines BeginInvoke in einem GUI-Thread wird die betreffende Aktion bis zur nächsten Verarbeitung von Windows-Nachrichten durch den UI-Thread verschoben. Dies kann in einigen Fällen sogar nützlich sein.
Supercat
2

Interessanterweise behandelt die Bindung von WPF das Marshalling automatisch, sodass Sie die Benutzeroberfläche an Objekteigenschaften binden können, die in Hintergrundthreads geändert wurden, ohne etwas Besonderes tun zu müssen. Dies hat sich für mich als große Zeitersparnis erwiesen.

In XAML:

<TextBox Text="{Binding Path=Name}"/>
gbc
quelle
das wird nicht funktionieren. Sobald Sie die Requisite auf den Nicht-UI-Thread gesetzt haben, erhalten Sie eine Ausnahme. dh Name = "gbc" bang! Misserfolg ... es gibt keinen freien Käsepartner
Boppity Bop
Es ist nicht kostenlos (es kostet Ausführungszeit), aber die wpf-Bindungsmaschinerie scheint Cross-Thread-Marshalling automatisch zu handhaben. Wir verwenden dies häufig bei Requisiten, die durch Netzwerkdaten aktualisiert werden, die in Hintergrundthreads empfangen wurden. Es gibt eine Erklärung hier: blog.lab49.com/archives/1166
gbc
1
@gbc Aaaaund Erklärung weg 404.
Jan 'splite' K.
0

Sie können versuchen, eine Art generische Komponente zu entwickeln, die einen SynchronizationContext als Eingabe akzeptiert und zum Aufrufen der Ereignisse verwendet.

Auf Freund
quelle