I have multiple serial devices connected to my PC and I am working on a program that allows users select as many as ports they want and then the program will dynamically creates TabPage and adds them to TabControl.
Each tab page will also have a multiline TextBox that will show the incoming data from the assigned serialport to it.
Here is my code that tries to create these controls dynamically:
private void AddSerialPort(string portName)
{
ActiveSerialPorts.Add(portName);
if (!tabControlActiveSerialPorts.Enabled)
tabControlActiveSerialPorts.Enabled = true;
var page = new TabPage(portName);
page.Text = portName;
var tb = new TextBox();
tb.Name = portName;
tb.Dock = DockStyle.Fill;
tb.BackColor = Color.Black;
tb.Multiline = true;
page.Controls.Add(tb);
tabControlActiveSerialPorts.TabPages.Add(page);
var sp = new SerialPort(portName, 115200, Parity.None, 8, StopBits.One);
sp.Open();
tb.Tag = sp;
sp.DataReceived += delegate
{
tb.Text += sp.ReadExisting(); //LINE 87
};
}
PROBLEM:
Here is the error I get on runtime, and break lands on line 87 (commented on code above):
Cross-thread operation not valid: Control 'COM16' accessed from a thread other than the thread it was created on.
What could be the possible pitfall here?
You're receiving data on background thread and trying to update the UI from the non-UI thread. You need to marshal the data from the background thread to the UI thread in order to update the control. This can be done using the Control.Invoke method.
sp.DataReceived += delegate
{
if (tb.InvokeRequired)
{
tb.Invoke(new Action(() =>
{
tb.Text += sp.ReadExisting();
}));
}
else
{
tb.Text += sp.ReadExisting();
}
}
Related
Similar to
MessageBox.Show("Test", "Test")
I have made a ProgressWindow, which is shown before a long-running action, and hidden thereafter:
ProgressWindow.Show("Test","Test")
Thread.Sleep(20000);
ProgressWindow.Hide();
using the following code:
class ProgressWindow : Form
{
private Label label1;
private static ProgressWindow window;
internal static void Show(string Message, string Caption)
{
window = new ProgressWindow(Message, Caption);
window.Show();
}
internal new static void Hide()
{
(ProgressWindow.window as Control).Hide();
}
private ProgressWindow(string Message, string Caption)
{
InitializeComponent(Message, Caption);
}
private void InitializeComponent(string Message, string Caption)
{
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// label1
//
this.label1.Location = new System.Drawing.Point(50, 40);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(300, 120);
this.label1.TabIndex = 0;
this.label1.Text = Message;
//
// ProgressWindow
//
this.ClientSize = new System.Drawing.Size(400, 200);
this.ShowIcon = false;
this.MinimizeBox = false;
this.MaximizeBox = false;
this.ControlBox = false;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.StartPosition = FormStartPosition.CenterScreen;
this.Controls.Add(this.label1);
this.Name = "ProgressWindow";
this.Text = Caption;
this.TopMost = true;
this.ResumeLayout(false);
}
}
The problem now is that my progress window is shown, but in the position of the label, there is a white box only, and no text. Furthermore, if I try to click the window, the title changes from "Test" to "Test (not responding...)".
Why is that, and how would I change that?
I suspected an issue with thread blocking (but why? shouldn't the label be rendered?) and tried
internal static void Show(string Message, string Caption)
{
window = new ProgressWindow(Message, Caption);
new Thread(t => {
window.Show();
}).Start();
}
but this doesn't show the ProgressWindow form at all.
Yes, the problem is due to thread blocking. When Thread.Sleep() is called, the current thread is doing nothing, which means that no Windows messages are processed. This prevents your progress dialog from completely displaying its UI.
I'm not exactly sure why calling the Show() method on a background thread doesn't work, but I believe that WinForms requires the UI thread to be a Single Threaded Apartment, and by default threads are Multi-Threaded Apartment.
To implement this properly, I would suggested using the BackgroundWorker class. It automatically creates a background thread to perform the long running work on, and will fire an even once the work completes. You can use it something like this:
ProgressWindow.Show("Test","Test");
var worker = new BackgroundWorker();
worker.DoWork += (sender, args) => {
// perform your long running task here, this is a background thread
Thread.Sleep(2000);
};
worker.RunWorkerCompleted += (sender, args) => {
// update the UI here, this is running on the UI thread
ProgressWindow.Hide();
}
worker.RunWorkerAsync();
Note that RunWorkerAsync() will return right away, so your UI will need to handle the fact that the user can interact with the UI before the background task has finished.
I've installed com0com so that I can write a NUnit test. Just in case there is a definition difference of Keyboard wedge a brief description of it is a piece of software that listens to a Serial communications device, reads any data sent to it (in my case formats it to ASCII data) then sends that to a Virtual keyboard. This code does work in production but we are required to now either document our code or have a unit test to prove how it is supposed to be used. so here is my test
[Test()]
public void WedgeSendsTextToVirtualKeyboardTest()
{
(var form = new Form())
using(var sp = new System.IO.Ports.SerialPort("COM"+COMB, 115200))
using (var wedge = new KeyboardWedgeConfiguration(WEDGE_KEY))
{
sp.Open();
TextBox tb = SetupForm(form);
TurnOnKeyboardWedge(wedge);
form.Activate();
form.Activated += (s, e) =>
{
tb.Focus();
};
while (!tb.Focused) { }
string str = "Hello World";
sp.Write(str);
//wait 1 second. This allows data to send, and pool in the wedge
//the minimum wait time is 200ms. the string then gets put into bytes
//and shipped off to a virtual keyboard where all the keys are pressed.
System.Threading.Thread.Sleep(1000);
Expect(tb.Text, Is.EqualTo(str));
}
}
private static TextBox SetupForm(Form form)
{
TextBox tb = new TextBox();
tb.Name = "tb";
tb.TabIndex = 0;
tb.AcceptsReturn = true;
tb.AcceptsTab = true;
tb.Dock = DockStyle.Fill;
form.Controls.Add(tb);
form.Show();
return tb;
}
private static void TurnOnKeyboardWedge(KeyboardWedgeConfiguration wedge)
{
wedge.Port = COMA;
wedge.PortForwardingEnabled = true;
wedge.Baud = 115200;
System.IO.Ports.SerialPort serialPort;
wedge.StartRerouting();
Assert.IsTrue(wedge.IsAlive(out serialPort));
Assert.IsNotNull(serialPort);
}
When the test runs, the form shows, no text is put in the textbox, then the test exits and the last assert fails (Expect(tb.Text, Is.EqualTo(str));) saying that tb.Text is string.Empty. I've tried a number of different tactics to get focus on that textbox (i'm assuming that is the problem atleast). At one time I made my sleep longer so that I had time to click on the textbox and type myself, and I couldn't click on the box (I'm assuming that is because of the sleep operation... which is also probably why my wedge can't type in there as well) so how can I fix this problem and make my test pass. Again this code does work in a production environment, so I am 100% convinced it is my test (and probably that sleep operation)
I was able to make my test pass with the help of this question stackoverflow question (thank you so much Patrick Quirk). It is actually a minor variation to it. I'm not even sure if my solution is 100% correct, but when the form pops up the text is entered and my test passes. The solution was two part system. First I had to make a class that extends Form override the Text property, and listen for the Activated and FormClosing Events. On Form Closing I would set my text, and on Activated I told my TextBox to have the focus.
private class WedgeForm : Form
{
public override string Text { get { return text; } set { text = value; } }
string text = string.Empty;
private TextBox tb;
public WedgeForm()
{
InitializeControls();
Activated += (s, e) => { tb.Focus(); };
FormClosing += (s, e) => { this.Text = tb.Text; };
}
private void InitializeControls()
{
tb = new TextBox();
tb.Name = "tb";
tb.TabIndex = 0;
tb.AcceptsReturn = true;
tb.AcceptsTab = true;
tb.Multiline = true;
tb.Dock = DockStyle.Fill;
this.Controls.Add(tb);
}
}
then using the InvokeEx method that was provided in the other qustion/answer My test easy to setup
[Test()]
public void WedgeSendsTextToVirtualKeyboardTest()
{
using (var form = new WedgeForm())
using (var wedge = new KeyboardWedgeConfiguration(WEDGE_KEY))
{
TurnOnKeyboardWedge(wedge);
string actual = MakeWedgeWriteHelloWorld(form, wedge); ;
string expected = "Hello World";
Expect(actual, Is.EqualTo(expected));
}
}
private static string MakeWedgeWriteHelloWorld(WedgeForm form, KeyboardWedgeConfiguration wedge)
{
var uiThread = new Thread(() => Application.Run(form));
uiThread.SetApartmentState(ApartmentState.STA);
uiThread.Start();
string actual = string.Empty;
var thread = new Thread
(
() => actual = InvokeEx<Form, string>(form, f => f.Text)
);
using (var sp = new System.IO.Ports.SerialPort("COM" + COMB, 115200))
{
sp.Open();
sp.Write("Hello World");
}
//wait 1 second. This allows data to send, and pool in the wedge
//the minimum wait time is 200ms. the string then gets put into bytes
//and shipped off to a virtual keyboard where all the keys are pressed.
Thread.Sleep(1000);
InvokeEx<Form>(form, f => f.Close());
thread.Start();
uiThread.Join();
thread.Join();
return actual;
}
One minor thing I have to remember when running this test is to not be clicking around because if that textbox loses focus I'm sunk. But the test is only 1second long.. I think I'll live.
In my C# WinForms app, I have the MainForm in which I am using a BackgroundWorker to read a large Text file.
I am also using a second Form to display a Marquee ProgressBar to inform user that they must wait until file has been completely read.
The problem I am having is the Form with the progressbar (SimpleProgressBar) is frozen until the file is read. Where the BW should be on a separate thread to not let this happen.
I left the code that reads the file as it may be relevant to my problem. However all the code actually works its just that the Form to display the ProgressBar is frozen.
SimpleProgressBar.cs (Form)
//Simple Progress Bar set to Marquee
public partial class SimpleProgressBar : Form
{
public SimpleProgressBar()
{
InitializeComponent();
//ProgressBar is setup in the designer.
/*
System.Windows.Forms.ProgressBar progressBar1;
this.progressBar1.Location = new System.Drawing.Point(16, 65);
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(350, 23);
this.progressBar1.Style = System.Windows.Forms.ProgressBarStyle.Marquee;
this.progressBar1.TabIndex = 1;
*/
}
}
MainForm.cs (Form)
//Class variable
private SimpleProgressBar wait = new SimpleProgressBar();
private void generatePreview()
{
//Setup BW Thread
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
//Start procesing file
worker.RunWorkerAsync();
//Show the dialog
wait.ShowDialog(); //perhaps .Show() would be better ?
}
//Once completed put the text into the textbox
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
textBox1.Text = ((StringBuilder)e.Result).ToString();
}
//Report progress here
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
int bufferSize = 1024;
var sb = new StringBuilder();
var buffer = new Char[bufferSize];
var length = 0L;
var totalRead = 0L;
var count = bufferSize;
using (var sr = new StreamReader("c:\200mb_text_file.txt"))
{
if (bw.CancellationPending)
{
}
else
{
length = sr.BaseStream.Length;
while (count > 0)
{
count = sr.Read(buffer, 0, bufferSize);
sb.Append(buffer, 0, count);
totalRead += count;
}
}
}
e.Result = sb;
}
UPDATE
So essentially I wanted to create a generic 2nd Form to use a a progress indicator that can be used for multiple purposes.
However it looks like that the BW MUST be on the same thread that hold the UI elements that need updating.
ShowDialog shows a modal dialog that you'll have to explicitly close. When you call ShowDialog, the program will not continue to execute beyond that point. Your background worker's completion event will have to close the dialog.
In addition, you're reading 1,024 bytes at a time and then calling the progress event. Every call to the progress event requires marshaling to the UI thread. It takes approximately zero time to read 1,024 bytes, which means that the progress event is being called continually, which in turn means that the UI thread is nearly 100% occupied with your progress update.
If you really need to report progress as the thing is loading, then use a larger buffer. You'll get better read performance anyway with a 64 Kilobyte buffer. That is:
int bufferSize = 65536;
But your files must be huge. You should be able to read at least 50 megabytes per second unless you have a really slow disk or you're reading over a slow network.
Progress Bar needs to be updated in another Thread.Updating the progress bar from the UI thread will cause freezing issues. Just put the code to update the progres bar in the Backgroudnworker's DOWork Method rather than reporting it.
void worker_DoWork(object sender, DoWorkEventArgs e)
{
stuffdone()
progressBar1.PerformStep();
}
Before using this you will need to set Form.CheckForIllegalCrossThreadCalls = false; so that BW can access the UI
I have the following C# program with a button called GetForceButton and a multiline textbox called ForceTextbox. Here is the code I have at the moment:
public Form1()
{
InitializeComponent();
System.ComponentModel.IContainer components = new System.ComponentModel.Container();
serialPort1 = new System.IO.Ports.SerialPort(components);
serialPort1.PortName = "COM7";
serialPort1.BaudRate = 9600;
serialPort1.DtrEnable = true;
serialPort1.Open();
serialPort1.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
}
bool buttonpressed = false;
public void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
string indata = sp.ReadLine();
if (buttonpressed == true)
{
ForceTextbox.Text = indata + "\n";
}
else
{
ForceTextbox.Text = "No data received";
}
}
private void GetForceButton_Click(object sender, EventArgs e)
{
buttonpressed = true;
}
When I step through the code, indata is getting the value from the serialPort of "0.00\r" (including the speech brackets).
After stepping to the ForceTextbox.Text = indata + "\n"; line, an exception is being thrown up saying:
An unhandled exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll. Additional information: Cross-thread operation not valid: Control 'ForceTextbox' accessed from a thread other than the thread it was created on.
What does that mean, or what am I doing wrong please?
You need to read this link.
The long and short of it is you need to make sure that you update GUI components on the same thread that started them. Mostly, this is done by the GUI thread.
You'll be using InvokeRequired as shown in that link.
You have to do this in C# all over the place unfortunately.
One other tutorial from Microsoft.
I want to get alive or dead ip addresses from a big lan. but it takes so many times. I decided to use backgroundworker here is my code:
try
{
this.Status.Text = "Collecting Information...";
if(this.TxtWorkGroup.Text.Trim() == "")
{
MessageBox.Show("The Work Group name Should Not be Empty");
return;
}
// Use Your work Group WinNT://&&&&(Work Group Name)
DirectoryEntry DomainEntry = new DirectoryEntry("WinNT://" + this.TxtWorkGroup.Text.Trim());
DomainEntry.Children.SchemaFilter.Add("computer");
// To Get all the System names And Display with the Ip Address
foreach(DirectoryEntry machine in DomainEntry.Children)
{
string[] Ipaddr = new string[3];
Ipaddr[0] = machine.Name;
System.Net.IPHostEntry Tempaddr = null;
try
{
Tempaddr = (System.Net.IPHostEntry)Dns.GetHostByName(machine.Name);
}
catch(Exception)
{
//MessageBox.Show("Unable to connect woth the system :" + machine.Name );
deadHostList.Items.Add(machine.Name);
continue;
}
System.Net.IPAddress[] TempAd = Tempaddr.AddressList;
foreach(IPAddress TempA in TempAd)
{
Ipaddr[1] = TempA.ToString();
byte[] ab = new byte[6];
int len = ab.Length;
// This Function Used to Get The Physical Address
int r = SendARP( (int) TempA.Address, 0, ab, ref len );
string mac = BitConverter.ToString( ab, 0, 6 );
Ipaddr[2] = mac;
}
System.Windows.Forms.ListViewItem TempItem = new ListViewItem(Ipaddr);
this.ListHostIP.Items.Add(TempItem);
}
this.Status.Text = "Displayed";
}
catch(Exception ex)
{
MessageBox.Show(ex.Message,"Error",System.Windows.Forms.MessageBoxButtons.OK );
Application.Exit();
}
but when I try to use these codes in backgroundWorker1_DoWork event it gives me error messages
Cross-thread operation not valid: Control 'deadHostList' accessed from a thread other than the thread it was created on
How can I modify my codes?
As Chris and Reed stated....you can only modify a Ui control from the thread that the control was created on...
You can also use the BackgroundWorker's ProgressChanged event for Ui updates....
var worker = new BackgroundWorker()
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
/// runs on background thread
worker.DoWork += (s, e) =>
{
while (!done)
{
DoSomeWork();
// send a message to the Ui
// via the ProgressChanged event
worker.ReportProgress(percent, statusMessage);
}
};
/// the ProgressChanged event runs on the UI thread
worker.ProgressChanged += (s, e) =>
{
var msg = (MyStatusMessage)e.UserState;
someUiControl.Items.Add(msg.Text);
};
/// also runs on Ui thread
worker.RunWorkerCompleted += (s, e) =>
{
};
worker.RunWorkerAsync();
You cannot and should not access UI controls from any thread other than the thread that created the control. I guess your deadHostList is a ListView control or something similar.
You can marshal a request from the background thread to the UI thread by using Control.Invoke or Control.BeginInvoke
You need to always marshal calls to UI elements, such as your list control, onto the UI thread.
You can do this via Control.Invoke. Change this:
deadHostList.Items.Add(machine.Name);
To:
string name = machine.Name;
deadHostList.Invoke(new Action( () => deadHostList.Items.Add(name)));
You'll also need to do the same thing later for ListHostIP - make sure to use Control.Invoke to wrap that call as well.