Reputation: 61
I am creating a windows forms program with a progress bar that will process a file with many rows. When I execute the following code, the progressBar1.Maximum call in the button2_Click method executes just fine but the one in the PostIncident method results in a System.InvalidOperationException which states: "Cross-thread operation not valid: Control 'progressBar1' accessed from a thread other than the thread it was created on."
A few avenues that I have tried so far: 1) Making PostIncident return a bool or magic value so that I pull the progressbar1.Maximum call up into the button2_Click method. My issues there is my skill with threading is not sufficient to get around the problems with capturing a returned value from a thread.
2) Tried putting a lock or semaphore around the progressbar1.Maximum call in the PostIncident method which resulted in the same error.
At the moment my solution is to simply remove threading from the project entirely but I'm sure there is probaly an elegant solution I am simply too inexperienced to see.
public partial class ExperianTriggerPoster : Form
{
private readonly OpenFileDialog _ofd = new OpenFileDialog();
public delegate void BarDelegate();
private string _path;
private void button1_Click(object sender, EventArgs e)
{
if (_ofd.ShowDialog() != DialogResult.OK) return;
textBox1.Text = _ofd.SafeFileName;
_path = _ofd.FileName;
}
private void button2_Click(object sender, EventArgs e)
{
string[] sAllLinesFromFile = File.ReadAllLines(_path);
foreach (string line in sAllLinesFromFile)
{
if (!line.StartsWith("N"))
{
progressBar1.Maximum -= 1;
continue;
}
//some logic here...
ThreadPool.QueueUserWorkItem(x => PostIncident(//some parameters here...));
}
}
private void PostIncident(//some parameters here...)
{
//some logic here...
if (customerNo == "not found") // must find a way to make this call thread-safe
{
Log.Information("Could not find customer# for user#: " + userNo);
progressBar1.Maximum -= 1;
}
Invoke(new BarDelegate(UpdateBar));
}
private void UpdateBar()
{
progressBar1.Value++;
if (progressBar1.Value != progressBar1.Maximum) return;
var postingComplete = MessageBox.Show("The posting is complete!", "Experian Trigger Poster", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
if (postingComplete == DialogResult.OK) Environment.Exit(0);
}
}
Upvotes: 3
Views: 187
Reputation: 3134
It is so simple.
Invoke(new BarDelegate(() => progressBar1.Maximum -= 1));
as you know using
Invoke(new BarDelegate(UpdateBar));
Remember that only UI thread gets access to UI controls. Trying to assign or read any property of a UI control in non-UI thread, you get an exception.
In your case, ThreadPool.QueueUserWorkItem
runs PostIncident
in a seperated background thread. In order to manipulate UI controls, just wrap statement with Invoke
.
Invoke
brings statements into UI thread and executes statements and then continues back in non-UI thread. It acts as a bridge between UI and non-UI threads.
Moreover, BeginInvoke
can be used if you don't want UI thread slowing your background thread. BeginInvoke
does not wait UI thread execution before continuing non-UI thread. Just be careful of race issue.
Upvotes: 1
Reputation: 693
Since PostIncident() is being run in a separate thread, it does not have access to update the values in the form. This can only be done from the UI thread.
There are a number of ways around this, but the standard way can be found in this article:
How to: Make Thread-Safe Calls to Windows Forms Controls
Google or search StackOverflow for "InvokeRequired" for more information.
Upvotes: 1
Reputation: 5141
Take a look on Safe, Simple Multithreading in Windows Forms or How to: Make Thread-Safe Calls to Windows Forms Controls.
You would like to invoke UI updates on the UI thread.
Breakdown:
Dunno if this is too advanced for you but this will really help you code in best practices.
Upvotes: 2