Jak zabezpieczyć wywołania zwrotne zdarzeń w moim wątku win forms?

Kiedy subskrybujesz Zdarzenie na obiekcie z poziomu formularza, zasadniczo przekazujesz kontrolę nad metodą wywołania zwrotnego źródłu zdarzenia. Nie masz pojęcia, czy źródło zdarzenia wybierze uruchomienie zdarzenia w innym wątku.

Problem polega na tym, że po wywołaniu wywołania zwrotnego nie można założyć, że można wprowadzić kontrolki aktualizacji w formularzu, ponieważ czasami te kontrolki rzucają expection, jeśli wywołanie zwrotne zdarzenia zostało wywołane w innym wątku niż wątek, na którym został uruchomiony formularz.

Author: Alex Miller, 2008-08-08

6 answers

Aby nieco uprościć kod Simona, możesz użyć wbudowanego generic Action delegate. Oszczędza to Pieprzenie kodu kilkoma typami delegatów, których naprawdę nie potrzebujesz. Ponadto, w. NET 3.5 dodali parametr params do metody Invoke, więc nie musisz definiować tymczasowej tablicy.

void SomethingHappened(object sender, EventArgs ea)
{
   if (InvokeRequired)
   {
      Invoke(new Action<object, EventArgs>(SomethingHappened), sender, ea);
      return;
   }

   textBox1.Text = "Something happened";
}
 31
Author: Jake Pearson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-08-12 15:59:51

Oto najważniejsze punkty:

  1. nie można wykonywać wywołań sterujących interfejsu użytkownika z innego wątku niż ten, na którym zostały utworzone (wątek formularza).
  2. wywołania delegatów (tzn. Hooki zdarzeń) są uruchamiane w tym samym wątku co obiekt uruchamiający Zdarzenie.

Więc, jeśli masz osobny wątek "engine" wykonujący jakąś pracę i masz jakiś interfejs Obserwujący zmiany stanu, które mogą być odzwierciedlone w interfejsie (np. pasek postępu lub cokolwiek), masz problem. Pożar silnika to obiekt, który został uzależniony od formy. Ale odpowiedź zwrotna, że Formularz zarejestrowany w silniku jest wywoływany na wątku silnika ... nie na wątku formularza. Nie możesz więc aktualizować żadnych kontrolek z tego połączenia zwrotnego. Doh!

BeginInvoke przychodzi na ratunek. Wystarczy użyć tego prostego modelu kodowania we wszystkich metodach oddzwaniania i możesz być pewien, że wszystko będzie dobrze:

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";
}
To dość proste. naprawdę.
  1. Użyj InvokeRequired, aby dowiedzieć się, czy to wywołanie zwrotne miało miejsce w prawidłowym wątku.
  2. Jeśli nie, uruchom ponownie wywołanie zwrotne na prawidłowym wątku z tymi samymi parametrami. Możesz ponownie uruchomić metodę za pomocą metod Invoke (blocking) lub BeginInvoke (non-blocking).
  3. przy następnym wywołaniu funkcji, InvokeRequired zwraca false, ponieważ jesteśmy teraz na właściwym wątku i wszyscy są szczęśliwi.

Jest to bardzo kompaktowy sposób rozwiązania tego problemu i uczynienia formularzy bezpiecznymi od wielowątkowych wywołań zwrotnych zdarzeń.

 16
Author: Simon Gillbee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-08-08 17:35:40

W tym scenariuszu często używam metod anonimowych:

void SomethingHappened(object sender, EventArgs ea)
{
   MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
   InvokeRequired ? Invoke( del ) : del(); 
}
 9
Author: Jason Diller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-21 18:31:58

Jestem trochę spóźniony na ten temat, ale może warto przyjrzeć się Asynchronicznemu wzorcowi opartemu na zdarzeniach . Po prawidłowym wdrożeniu gwarantuje, że zdarzenia są zawsze podnoszone z wątku UI.

Oto krótki przykład, który pozwala tylko na jedno jednoczesne wywołanie; obsługa wielu wywołań / zdarzeń wymaga trochę więcej instalacji hydraulicznej.

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; }
        }
    }
}
 2
Author: OwenP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-04 20:08:40

Jako lazy programmer, mam na to bardzo leniwy sposób.

To, co robię, to po prostu to.
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; });
}

Możesz wstawić DoInvoke wewnątrz swojej funkcji lub ukryć go w oddzielnej funkcji, aby wykonać brudną robotę za Ciebie.

Pamiętaj, że możesz przekazać funkcje bezpośrednio do metody DoInvoke.

private void directPass() {
    DoInvoke(this.directInvoke);
}
private void directInvoke() {
    textLabel.Text = "Directly passed.";
}
 1
Author: Chase,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-30 21:57:58

W wielu prostych przypadkach można użyć delegata MethodInvoker i uniknąć konieczności tworzenia własnego typu delegata.

 0
Author: Chris Farmer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-08-08 17:41:18