views:

921

answers:

2

Hello, I am trying to listen to COM port so that I create new handler for SerialPort.DataReceived event. The logic is simple - I write something to TextBox1, press Button1 and my text should show it self in Label1. But my application don't want to run, becouse it throws 'Cross thread operation not valid' error. I did some searching and found Invoke object - how can I use it in my example? Why do I need to include Invoke logic?

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.IO.Ports;

namespace WindowsApplication1
{
public partial class Form1 : Form
{
    SerialPort sp = new SerialPort();

    public Form1()
    {
        InitializeComponent();
        sp.DataReceived += MyDataReceivedHandler;
    }

    private void Form1_Load(object sender, EventArgs e)
    {

    }

    private void MyDataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
    {
        try
        {
            //sp.PortName = "COM3";
            //sp.Open();
            Label1.Text = sp.ReadLine();
        }
        catch (Exception exception)
        {
            RichTextBox1.Text = exception.Message + "\n\n" + exception.Data;
        }
        finally
        {
            sp.Close();
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        try
        {
            sp.PortName = "COM3";
            sp.Open();
            sp.WriteLine(TextBox1.Text);
        }
        catch (Exception exception)
        {
            RichTextBox1.Text = exception.Message + "\n\n" + exception.Data;
        }
        finally
        {
            sp.Close();
        }
    }
}

}

+10  A: 

My guess is that MyDataReceivedHandler is running on a different thread than the GUI. In order to fix that, you need to invoke the Text setters on the correct thread. This is a sample of doing so:

public void SetControlText(Control control, string text)
{
    if (this.InvokeRequired)
    {
        this.Invoke(new Action<Control,string>(SetControlText), new object[] { control, text });
    }
    else
    {
        control.Text = text;
    }
}

private void MyDataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
    try
    {
        //sp.PortName = "COM3";
        //sp.Open();
        SetControlText(Label1, sp.ReadLine());
    }
    catch (Exception exception)
    {
        SetControlText(RichTextBox1, exception.Message + "\n\n" + exception.Data);
    }
    finally
    {
        sp.Close();
    }
}

If you are using .NET Framework 2.0, the above Action<T1, T2> delegate is not available, so you will have to define your own one:

private delegate void SetControlTextHandler(Control control, string text);

public void SetControlText(Control control, string text)
{
    if (this.InvokeRequired)
    {
        this.Invoke(new SetControlTextHandler(SetControlText), new object[] { control, text });
    }
    else
    {
        control.Text = text;
    }
}

The SetControlText method can be made shorter (and even static) like this (this works in both 2.0 and 3.5):

public static void SetControlText(Control control, string text)
{
    ´control.Invoke((MethodInvoker)delegate { control.Text = text; });
}

Then you don't need to do the check of InvokeRequired each time, but you will on the other hand wrap the call in a delegate even if it is not needed. I think that in a GUI method like this any performance difference between those two is neglectible so I tend to use the shorter form, simply because it is less code to write.

Fredrik Mörk
It seems, that this works only in 3.5. I use Visual studio 2005, now I installed 3.5 SP1. Where in Visual studio 2005 can I set which .NET framework I am using?
_simon_
@_simon_: I have updated the answer with 2.0 compatible versions
Fredrik Mörk
Note: If the operation performed in the delegate is long running, it can still block the UI as the invoke will just cause the UI thread to process the operation. Using BeginInvoke on all controls that implement it will perform the operation asynchronously, without blocking.
SnOrfus
+1 and should you not call the invoke/begininvoke on the control, and not on 'this' (the form)?
SnOrfus
A: 

You can also do the following whenever accessing a UI control from a different thread than the one it was created on:

(.NET 3.5)

myControl.BeginInvoke(new MethodInvoker( () => myControl.whatever = whatever; ));

or (.NET 2.0)

myControl.BeginInvoke(new MethodInvoker( delegate { myControl.whatever = whatever; ));

edit> Sometimes using Invoke for a long running operation can/will still hang the ui, using BeginInvoke obviously performs that operation asynchronously, and the ui will not hang.

SnOrfus