views:

2784

answers:

5

When you subscribe to an event on an object from within a form, you are essentially handing over control of your callback method to the event source. You have no idea whether that event source will choose to trigger the event on a different thread.

The problem is that when the callback is invoked, you cannot assume that you can make update controls on your form because sometimes those controls will throw an expection if the event callback was called on a thread different than the thread the form was run on.

+8  A: 

Here are the salient points:

  1. You can't make UI control calls from a different thread than the one they were created on (the form's thread).
  2. Delegate invocations (ie, event hooks) are triggered on the same thread as the object that is firing the event.

So, if you have a separate "engine" thread doing some work and have some UI watching for state changes which can be reflected in the UI (such as a progress bar or whatever), you have a problem. The engine fire's an object changed event which has been hooked by the Form. But the callback delegate that the Form registered with the engine gets called on the engine's thread… not on the Form's thread. And so you can't update any controls from that callback. Doh!

BeginInvoke comes to the rescue. Just use this simple coding model in all your callback methods and you can be sure that things are going to be okay:

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";
}

It's quite simple really.

  1. Use InvokeRequired to find out if this callback happened on the correct thread.
  2. If not, then reinvoke the callback on the correct thread with the same parameters. You can reinvoke a method by using the Invoke (blocking) or BeginInvoke (non-blocking) methods.
  3. The next time the function is called, InvokeRequired returns false because we are now on the correct thread and everybody is happy.

This is a very compact way of addressing this problem and making your Forms safe from multi-threaded event callbacks.

Simon Gillbee
I generally prefer BeginInvoke to Invoke, but there's a caveat: one must avoid queueing up too many events. I use an updateRequired variable which is set to 1 when a BeginInvoke would happen, and only perform the BeginInvoke if it had been zero (using Interlocked.Exchange). The display handler has a while loop that clears updateRequired and, if it wasn't zero, does an update and loops. In some cases, a timer is added to further limit update frequency (to avoid having code spend all its time updating the progress readout instead of doing real work) but that's more complicated.
supercat
@Supercat... event throttling is an important topic for many applications, but it is not something that should be part of the UI layer. A separate event proxy bus should be created to receive, queue, combine and resend events at appropriate intervals. Any subscriber to the event bus should not know that event throttling is occurring.
Simon Gillbee
@Simon Gillbee: I can see places where a separate "event bus" to handle synchronization could be useful, but in many cases it would seem easiest for the end user of something like a progress-indicator class if the class simply exposed a MinimumUpdateInterval property.
supercat
A: 

In many simple cases, you can use the MethodInvoker delegate and avoid the need to create your own delegate type.

Chris Farmer
+8  A: 

To simplify Simon's code a bit, you could use the built in generic Action delegate. It saves peppering your code with a bunch of delegate types you don't really need. Also, in .NET 3.5 they added a params parameter to the Invoke method so you don't have to define a temporary array.

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

   textBox1.Text = "Something happened";
}
Jake Pearson
+4  A: 

I use anonymous methods a lot in this scenario:

void SomethingHappened(object sender, EventArgs ea)
{
   MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
   InvokeRequired ? Invoke( del ) : del(); 
}
Jason Diller
+1  A: 

I'm a bit late to this topic, but you might want to take a look at the Event-Based Asynchronous Pattern. When implemented properly, it guarantees that events are always raised from the UI thread.

Here's a brief example that only allows one concurrent invocation; supporting multiple invocations/events requires a little bit more plumbing.

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; }
        }
    }
}
OwenP
That's really good to know. Thanks for the information.
Simon Gillbee