2

I have some code that I wrote, which does what I want. However, I am not quite sure how, exactly, it works. The part I am having the most trouble with is the last part. I had a textBox1.Text = "test" which did not work. I got a run time error about it being called from a different thread. When I put the textBox1.Invoke(etc etc), it worked as expected. Why?

As you can see, I know just enough to be dangerous and I really want to understand what's going on here instead of blindly copying and pasting from sites around the web.

I have the following in a class named SerialCommunicator:

public SerialCommunicator(SerialPort sp)
{
    this.sp = sp;
    sp.ReceivedBytesThreshold = packetSize;
    sp.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
    sp.Open();
}    
public void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    Thread.Sleep(50);
    SerialPort s = (SerialPort)sender;
    byte[] buffer = new byte[128];
    s.Read(buffer, 0, s.BytesToRead);
}

Then, in my Form1.cs I have a button that when pressed does the following:

private void btnPortOK_Click(object sender, EventArgs e)
{
    string comPort = cboComPorts.SelectedItem.ToString();
    SerialPort sp = new SerialPort(comPort, 9600, Parity.None, 8, StopBits.One);
    sp.DataReceived += new SerialDataReceivedEventHandler(DataHasBeenReceived);
    comm = new SerialCommunicator(sp);
}
public void DataHasBeenReceived(object sender, EventArgs args)
{
    textBox1.Invoke(new EventHandler(delegate { textBox1.Text += "test"; }));
}
Pete
  • 10,651
  • 9
  • 52
  • 74

5 Answers5

4

This is thread-affinity. UI controls don't like to be touched by anything except the thread that created them, but the DataReceived thread happens from a different thread. Adding a call toControl.Invoke pushes an item of work back to the UI thread, so the Text updated can succeed.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
2

I am not an expert on this (there will likely be better answers than this). But as I understand it, the GUI thread "owns" your form. So when you try to update it from a different thread you are crossing the streams.

The Invoke is a way to ask the GUI thread to run a method. Method that it runs is your textBox1.Text += "test";

The idea is by invoking a delegate, that will ask the GUI thread to make the change, rather than just changing the value yourself. This allows allow the change to be done in a thread safe manner.

Here is a good article by Jon Skeet on this issue: http://www.yoda.arachsys.com/csharp/threads/winforms.shtml

Vaccano
  • 78,325
  • 149
  • 468
  • 850
  • +1 for the link. I'll accept this answer or any answer that explains the part passed into Invoke (it looks to me like invoke takes a delegate according to MSDN but here its being passed an EventHandler type which has a delegate as its parameter). As you can see, I want to "do something" when data is received on the serial port. Can that textBox1.Invoke(etc...) be written "more simply"? – Pete Feb 23 '11 at 07:08
  • Control.Invoke takes a delegate. EventHandler is a type of delegate (Docs here: http://msdn.microsoft.com/en-us/library/db0etb8x.aspx) so it can take that if you want. Or you could just pass in lambda expression (cast as a delegate) without the EventHandler wrapper. The most compressed way I can think of to write your code would be like this: `textBox1.Invoke((Action)(()=> textBox1.Text += "test"));` – Vaccano Feb 23 '11 at 07:26
  • Jon Skeet shows how you could use an extension method if you want to avoid the cast here: http://stackoverflow.com/questions/411579/why-must-a-lambda-expression-be-cast-when-supplied-as-a-plain-delegate-parameter/411597#411597 – Vaccano Feb 23 '11 at 07:27
1

Events are called from the thread where they happen. (Unless specified otherwise).

Think about this way:
When you activate the event, it is actually called as a finction EventName(). So calling an event means actually going to all the methods that were registered to that event and doing them.
But, this is done in the same thread in a serial way.

So if an event happened in a thread that is not your UI thread you'll get theat error.

Yochai Timmer
  • 48,127
  • 24
  • 147
  • 185
1

textBox1.Text = "test" doesn't work because you are calling it from another thread (i.e. the DataHasBeenReceived event) then the thread who owns the textbox. That's usually the thread in which your application runs and that creates your GUI interface (and thus your textbox). Invoke works because that methods switches to the GUI thread, sets your text and then switches back to the thread of your DataHasBeenReceived event.

In Net 1.0 and 1.1 you could use GUI controls from another thread then then the one that owned them but this resulted in a lot of problems when threads started accessing the controls at the same time. So, since net 2.0 Microsoft changed that.

If you want to know if must use invoke or not (i.e. if a method can be called from the both the GUI thread or another thread), you can use the property InvokeRequired combined with an if else. A invoke call is slightly more expensive then a direct manipulation of the control.

Sem Vanmeenen
  • 2,111
  • 11
  • 13
1

The issue is that the GUI components only accepts modifications from the GUI thread. So when other threads want to modify the GUI, then they must queue their modification code using measures like control.Invoke(...) which will queue the delegate to be processed as soon as possible on the GUI event queue, and thus the correct thread.

What you run in to is that one of the built-in checks are fired than controls that the calling thread indeed is the correct thread. It is a security measure that makes debugging easier (if they were not present you would have to debug subtle threading issues instead...)

Anders Zommarin
  • 7,094
  • 2
  • 25
  • 24