Wie mache ich Ereignisrückrufe in meinen Win Forms-Thread sicher?

  • Wenn Sie ein Ereignis für ein Objekt innerhalb eines Formulars abonnieren, übergeben Sie im Wesentlichen die Kontrolle über Ihre Callback-Methode an die Ereignisquelle. Sie haben keine Ahnung, ob diese Ereignisquelle das Ereignis in einem anderen Thread auslösen soll.

    Das Problem ist, dass beim Aufruf des Callbacks nicht davon ausgegangen werden kann, dass Sie eine Aktualisierung vornehmen können Steuerelemente in Ihrem Formular, da diese Steuerelemente manchmal eine Erwartung auslösen, wenn der Ereignisrückruf in einem anderen Thread als dem Thread aufgerufen wurde, in dem das Formular ausgeführt wurde.

    26 September 2008
    Alex Miller
6 answers
  • Um Simons Code ein wenig zu vereinfachen, können Sie den eingebauten generischen Action-Delegaten verwenden. Es erspart dem Peppern Ihren Code mit einer Reihe von Delegatentypen, die Sie nicht wirklich benötigen. In .NET 3.5 wurde der Invoke-Methode auch ein Parameter hinzugefügt, sodass Sie kein temporäres Array definieren müssen.

     void SomethingHappened(object sender, EventArgs ea)
    {
       if (InvokeRequired)
       {
          Invoke(new Action<object, EventArgs>(SomethingHappened), sender, ea);
          return;
       }
    
       textBox1.Text = "Something happened";
    }
     
    12 August 2008
    Jake Pearson
  • Hier sind die wichtigsten Punkte:

    1. Sie können keine UI-Kontrollaufrufe von einem anderen Thread aus als dem erstellen, für den sie erstellt wurden (Der Thread des Formulars).
    2. Delegieren von Aufrufen (dh Ereignis-Hooks) werden in demselben Thread ausgelöst wie das Objekt, das das Ereignis auslöst.

    Wenn Sie also einen separaten "Engine" -Thread haben, der etwas Arbeit leistet und einige UI auf Zustandsänderungen achten, die sich in der UI niederschlagen können ( B. ein Fortschrittsbalken oder was auch immer), Sie haben ein Problem. Das Triebwerksfeuer ist ein Objekt, das von einem Formular geändert wurde. Der Callback-Delegat, den das mit der Engine registrierte Formular erhält, wird jedoch vom Thread der Engine aufgerufen… nicht vom Thread des Formulars. Daher können Sie keine Steuerelemente von diesem Rückruf aus aktualisieren. Doh!

    BeginInvoke kommt zur Rettung. Verwenden Sie einfach dieses einfache Codiermodell in all Ihren Rückmeldemethoden, und Sie können sicher sein, dass alles in Ordnung ist:

    private delegate void EventArgsDelegate(object sender, EventArgs ea); void SomethingHappened(object sender, EventArgs ea) { // // Make sure this callback is on the correct thread // if (this.InvokeRequired) { this.Invoke(new EventArgsDelegate(SomethingHappened), new object[] { sender, ea }); return; } // // Do something with the event such as update a control // textBox1.Text = "Something happened"; }

    Es ist wirklich ganz einfach.

    1. Verwenden Sie InvokeRequired , um herauszufinden, ob dieser Rückruf im richtigen Thread aufgetreten ist. < / li>
    2. Wenn nicht, rufen Sie den Callback mit den gleichen Parametern erneut im richtigen Thread auf. Sie können eine Methode erneut aufrufen, indem Sie die Methoden Invoke (Blockieren) oder BeginInvoke (nicht blockieren) verwenden.
    3. Beim nächsten Mal wird die Funktion ausgeführt heißt, InvokeRequired gibt "false" zurück, da wir uns jetzt im richtigen Thread befinden und alle glücklich sind.

    Dies ist eine sehr kompakte Methode, um dieses Problem zu beheben und Ihre Forms vor Multithread-Ereignisrückrufen zu schützen.

    08 August 2008
    Simon Gillbee
  • Ich verwende in diesem Szenario häufig anonyme Methoden:

     void SomethingHappened(object sender, EventArgs ea)
    {
       MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
       InvokeRequired ? Invoke( del ) : del(); 
    }
     
    21 October 2008
    Jason Diller
  • Ich bin etwas spät dran bei diesem Thema, aber Sie möchten vielleicht einen Blick auf Ereignisbasiertes asynchrones Muster . Bei korrekter Implementierung wird sichergestellt, dass Ereignisse immer vom UI-Thread ausgelöst werden.

    Hier ein kurzes Beispiel, das nur einen gleichzeitigen Aufruf zulässt. Die Unterstützung mehrerer Aufrufe / Ereignisse erfordert etwas mehr Abfluss.

     using System;
    using System.ComponentModel;
    using System.Threading;
    using System.Windows.Forms;
    
    namespace WindowsFormsApplication1
    {
        public class MainForm : Form
        {
            private TypeWithAsync _type;
    
            [STAThread()]
            public static void Main()
            {
                Application.EnableVisualStyles();
                Application.Run(new MainForm());
            }
    
            public MainForm()
            {
                _type = new TypeWithAsync();
                _type.DoSomethingCompleted += DoSomethingCompleted;
    
                var panel = new FlowLayoutPanel() { Dock = DockStyle.Fill };
    
                var btn = new Button() { Text = "Synchronous" };
                btn.Click += SyncClick;
                panel.Controls.Add(btn);
    
                btn = new Button { Text = "Asynchronous" };
                btn.Click += AsyncClick;
                panel.Controls.Add(btn);
    
                Controls.Add(panel);
            }
    
            private void SyncClick(object sender, EventArgs e)
            {
                int value = _type.DoSomething();
                MessageBox.Show(string.Format("DoSomething() returned {0}.", value));
            }
    
            private void AsyncClick(object sender, EventArgs e)
            {
                _type.DoSomethingAsync();
            }
    
            private void DoSomethingCompleted(object sender, DoSomethingCompletedEventArgs e)
            {
                MessageBox.Show(string.Format("DoSomethingAsync() returned {0}.", e.Value));
            }
        }
    
        class TypeWithAsync
        {
            private AsyncOperation _operation;
    
            // synchronous version of method
            public int DoSomething()
            {
                Thread.Sleep(5000);
                return 27;
            }
    
            // async version of method
            public void DoSomethingAsync()
            {
                if (_operation != null)
                {
                    throw new InvalidOperationException("An async operation is already running.");
                }
    
                _operation = AsyncOperationManager.CreateOperation(null);
                ThreadPool.QueueUserWorkItem(DoSomethingAsyncCore);
            }
    
            // wrapper used by async method to call sync version of method, matches WaitCallback so it
            // can be queued by the thread pool
            private void DoSomethingAsyncCore(object state)
            {
                int returnValue = DoSomething();
                var e = new DoSomethingCompletedEventArgs(returnValue);
                _operation.PostOperationCompleted(RaiseDoSomethingCompleted, e);
            }
    
            // wrapper used so async method can raise the event; matches SendOrPostCallback
            private void RaiseDoSomethingCompleted(object args)
            {
                OnDoSomethingCompleted((DoSomethingCompletedEventArgs)args);
            }
    
            private void OnDoSomethingCompleted(DoSomethingCompletedEventArgs e)
            {
                var handler = DoSomethingCompleted;
    
                if (handler != null) { handler(this, e); }
            }
    
            public EventHandler<DoSomethingCompletedEventArgs> DoSomethingCompleted;
        }
    
        public class DoSomethingCompletedEventArgs : EventArgs
        {
            private int _value;
    
            public DoSomethingCompletedEventArgs(int value)
                : base()
            {
                _value = value;
            }
    
            public int Value
            {
                get { return _value; }
            }
        }
    }
     
    04 December 2008
    OwenP
  • Als lazy programmer habe ich eine sehr faule Methode dazu.

    Was ich mache, ist einfach das Folgende.

     private void DoInvoke(MethodInvoker del) {
        if (InvokeRequired) {
            Invoke(del);
        } else {
            del();
        }
    }
    //example of how to call it
    private void tUpdateLabel(ToolStripStatusLabel lbl, String val) {
        DoInvoke(delegate { lbl.Text = val; });
    }
     

    Sie können DoInvoke in Ihre Funktion einbetten oder es in einer separaten Funktion ausblenden, um die schmutzige Arbeit für Sie zu erledigen.

    Denken Sie daran, dass Sie Funktionen direkt an die DoInvoke-Methode übergeben können.

     private void directPass() {
        DoInvoke(this.directInvoke);
    }
    private void directInvoke() {
        textLabel.Text = "Directly passed.";
    }
     
    31 May 2012
    Chase
  • In vielen einfachen Fällen können Sie den MethodInvoker-Delegaten verwenden, sodass Sie keinen eigenen Delegattyp erstellen müssen.

    08 August 2008
    Chris Farmer