RS232 using thread-safe calls to Windows Forms controls






4.58/5 (27 votes)
Jan 21, 2007
2 min read

214363

7780
An article on RS232 using thread-safe calls to Windows Forms controls.
Introduction
As RS-232 is still used often in industrial applications, Microsoft has added the SerialPort
class to its .NET 2.0 framework.
When communicating with a Windows Forms application and displaying the received data, for instance in a TextBox
, the problem occurs, that the serial port and the textbox are using different threads. MSDN describes a way to solve this problem.
As an example of how to apply all this, using Visual C#, a small application has been built:
First of all, you have to place, from the Toolbox=>Components, a SerialPort
onto the Form.
Then, in the Properties, set the serial port to 9600,8N1 and the port name to X (don't be alarmed; after the application has started, you will get the opportunity to choose a proper COM-name!)
Now, place the top-textbox (txtIn
), showing received data, and the lower textbox (txtOut
), showing the data to be send after clicking the Send button. Optionally, you could also place a SttusStrip
(as I did).
Add a ComboBox
(cmbComSelect
) with DropDownStyle
set to DropDown
and Sorted
set to true
.
The Namespace
The System.IO.Ports
namespace contains classes for controlling serial ports. The most important class is the SerialPort
class.
The Code
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 RS232
{
public partial class fclsRS232Tester : Form
{
string InputData = String.Empty;
// This delegate enables asynchronous calls for setting
// the text property on a TextBox control:
delegate void SetTextCallback(string text);
public fclsRS232Tester()
{
InitializeComponent();
// Nice methods to browse all available ports:
string[] ports = SerialPort.GetPortNames();
// Add all port names to the combo box:
foreach (string port in ports)
{
cmbComSelect.Items.Add(port);
}
}
private void cmbComSelect_SelectionChangeCommitted(object sender,
EventArgs e)
{
if (port.IsOpen) port.Close();
port.PortName = cmbComSelect.SelectedItem.ToString();
stsStatus.Text = port.PortName + ": 9600,8N1";
// try to open the selected port:
try
{
port.Open();
}
// give a message, if the port is not available:
catch
{
MessageBox.Show("Serial port " + port.PortName +
" cannot be opened!", "RS232 tester",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
cmbComSelect.SelectedText = "";
stsStatus.Text = "Select serial port!";
}
}
private void btnSend_Click(object sender, EventArgs e)
{
if (port.IsOpen) port.WriteLine(txtOut.Text);
else MessageBox.Show("Serial port is closed!",
"RS232 tester",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
txtOut.Clear();
}
private void btnClear_Click(object sender, EventArgs e)
{
txtIn.Clear();
}
private void port_DataReceived_1(object sender,
SerialDataReceivedEventArgs e)
{
InputData = port.ReadExisting();
if (InputData != String.Empty)
{
// txtIn.Text = InputData;
// because of different threads this
// does not work properly !!
SetText(InputData);
}
}
/*
To make a thread-safe call a Windows Forms control:
1. Query the control's InvokeRequired property.
2. If InvokeRequired returns true, call Invoke with
a delegate that makes the actual call to the control.
3. If InvokeRequired returns false, call the control directly.
In the following code example, this logic is
implemented in a utility method called SetText.
A delegate type named SetTextDelegate encapsulates the SetText method.
When the TextBox control's InvokeRequired
returns true, the SetText method creates an instance
of SetTextDelegate and calls the form's Invoke method.
This causes the SetText method to be called
on the thread that created the TextBox control,
and in this thread context the Text property is set directly
also see: http://msdn2.microsoft.com/en-us/library/ms171728(VS.80).aspx
*/
// This method demonstrates a pattern for making thread-safe
// calls on a Windows Forms control.
//
// If the calling thread is different from the thread that
// created the TextBox control, this method creates a
// SetTextCallback and calls itself asynchronously using the
// Invoke method.
//
// If the calling thread is the same as the thread that created
// the TextBox control, the Text property is set directly.
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.txtIn.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else this.txtIn.Text += text;
}
}
}
Getting the COM-port
The SerialPort
gives us a nice method to find all COM-ports available in the computer:
string[] ports = SerialPort.GetPortNames();
Now that we have all ports available, we fill the Items
list of the ComboBox
:
foreach (string port in ports)
{
cmbComSelect.Items.Add(port);
}
and, through the SelectionChangeCommitted
event from the ComboBox
, we can choose a particular COM-port.
Because the selected COM-port may be in use, we try to open it, and if an exception is thrown, we catch it and display a proper message.
Achieving thread-safety
The statement txtIn.Text = InputData;
will cause problems on receiving data, because the Receive thread is different from the thread which started the textbox!!
To solve this problem in a thread-safe way, we use the following statement instead:
SetText(InputData);
First, we have to declare a delegate method:
delegate void SetTextCallback(string text);
and then this method can be defined as:
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.txtIn.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else this.txtIn.Text += text;
}