One of the most common frustrations of new .NET programmers revolves around an incomplete understanding of how the Windows Forms UI model works. A very simple example, to illustrate, comes from a recent post on one of our Eggheadcafe.com forums:
private void port_DataReceived(object sender, SerialDataReceivedEventArgs e) { textBox3.Text = "ı"; // "Cross-thread operation not valid" here }
The port_DataReceived event handler is being called on a secondary thread, but the user is attempting to update a control on the Main UI thread from it, and you cannot do that.
The Windows OS uses a single-threaded, message-processing based user interface, and any alternate thread interaction needs to be marshaled through the Windows message pump. Normally this involves checking a component's InvokeRequired property to determine if marshaling is necessary. You can call Invoke() directly and it will check this anyway, but it is more efficient to do it explicitly beforehand. Here is a complete example (minus of course, the progress bar and button you would need to drop onto the actual Form) that illustrates how this pattern might be used:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace WFInvoke
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
private void IncrementMe()
{
for (int i = 0; i < 100; i++)
{
UpdateProgress();
Thread.Sleep(100);
}
if(InvokeRequired)
{
Invoke(new MethodInvoker(Close));
}
else
{
Close();
}
}
private void UpdateProgress()
{
if (this.progressBar1.InvokeRequired)
{
MethodInvoker updateProgress = UpdateProgress;
progressBar1.Invoke(updateProgress);
}
else
{
progressBar1.Increment(1);
}
}
private void button1_Click(object sender, EventArgs e)
{
ThreadStart threadStart = IncrementMe;
threadStart.BeginInvoke(null, null);
}
}
}
We are running the IncrementMe method on a secondary thread, and we use the Invoke pattern to safely update the progress bar on the Main UI thread from inside the method.
BackgroundWorker Pattern
In .NET 1.1 it was necessary to wire up a lot of delegate-invoking to do something that turns out to be pretty common in Winforms development - to be able to be notified when a background thread method is complete, and also to be able to be notified periodically on its status. Being able to cancel a long-running background thread task is also desirable. This set of tasks is so common that the developers of the 2.0 Framework added the BackgroundWorker class, which makes all this much easier to do.
BackgroundWorker basically works like this:
1) You attach your long-running method to the BackgroundWorker's DoWork event. 2) To wire up progress notifications, you hook up an event listener to the BackgroundWorker class's ProgressChanged event, and set it's WorkerReportsProgress property to true. 3) You register your "Complete" method with the BackgroundWorker's RunWorkerCompleted event. 4) You can set the WorkerSupportsCancellation property to true, and a call to the BackgroundWorker.CancelAsync method will set the DoWorkEventArgs.CancellationPending flag, which you can then check and act accordingly in your long-running method that has been supplied to the BackgroundWorker class. 5) To start your method, you call the BackgroundWorker.RunWorkerAsync() method, passing a state parameter that is sent into your long-running method.
The only major drawback to using this substantially easier way to hook up all this eventing and reporting is that you cannot just use it with "any method" -- your method needs to conform to the System.ComponentModel.DoWorkEventHandler delegate, which requires arguments of type Object, and DoWorkEventArgs. Otherwise, a wrapper method will be required. In the example below, I illustrate the passing of multiple parameters by sending in an object[] array containing different types as the state parameter.
Aside from the above, you only need to remember to check EventArgs.Error inside the RunWorkerCompleted callback for any exception, otherwise the exception will be unreported; it doesn't even propagate to the AppDomain's UnhandledException event. Here is a somewhat trivial example, but it illustrates all the features described:
using System;
using System.Threading;
using System.ComponentModel;
class Program
{
static BackgroundWorker bwkr;
static void Main()
{
bwkr = new BackgroundWorker();
bwkr.WorkerReportsProgress = true;
bwkr.WorkerSupportsCancellation = true;
bwkr.DoWork += bwkr_DoWork;
bwkr.ProgressChanged += bwkr_ProgressChanged;
bwkr.RunWorkerCompleted += bwkr_RunWorkerCompleted;
object[] parms = {"one",2,DateTime.Now};
bwkr.RunWorkerAsync(parms);
Console.WriteLine("Press Enter during run to cancel.");
Console.ReadLine();
if (bwkr.IsBusy) bwkr.CancelAsync();
Console.ReadLine();
}
static void bwkr_DoWork(object sender, DoWorkEventArgs e)
{
object[] o = (object[])e.Argument;
foreach (object ob in o)
Console.WriteLine("arg: " + ob.ToString());
Console.WriteLine("==================================");
int tot = 0;
for (int i = 0; i <= 100; i += 10)
{
tot += i;
if (bwkr.CancellationPending)
{
e.Cancel = true;
return;
}
bwkr.ReportProgress(i);
Thread.Sleep(1000);
}
e.Result = tot; // This gets passed to RunWorkerCompleted
}
static void bwkr_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
Console.WriteLine("Canceled by User.");
else if (e.Error != null)
Console.WriteLine("Worker exception: " + e.Error.ToString());
else
Console.WriteLine("Complete - " + e.Result); // from DoWork
}
static void bwkr_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
Console.WriteLine("bwkr: " + e.ProgressPercentage + "%");
}
}
You can download this sample Visual Studio 2005 Solution containing both working examples. |