I have to think this is better than the MS article that recommends
polling:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpovrAsynchronousProgrammingOverview.asp

Please advise otherwise.

This snippet shows how a worker thread can be set up that properly
calls the parent form back. The basic jist is that an asynch delegate
is used to run the worker thread, and, the worker thread in turn uses
the forms BeginInvoke to effectively "PostMessage" back to the calling
thread.

To illustrate propogating exceptions from the worker thread to the
parent form, two buttons are used. One forces an error, and the other
does not.

IAsyncResult iarMe, iarForm;
WorkerThreadDelegate workerThreadDelegate;

public delegate void WorkerThreadDelegate( int start );

private void WorkerThread( int start )
{
int k = 1;
System.Exception exc = null;
try
{
for (int i = start; i < start + 1000000000; i++)
{
k = k + 1;
}
if (start > 0)
{
throw new System.Exception( "Hello" );
}
}
catch (System.Exception _exc)
{
exc = _exc;
}

iarForm = this.BeginInvoke( new ThreadFinishedHandler( ThreadFinished
), new object[] { k, exc } );
}

private delegate void ThreadFinishedHandler( int k, Exception exc );

private void ThreadFinished( int k, Exception exc )
{
EndInvoke( iarForm );

workerThreadDelegate.EndInvoke( iarMe );

button1.Enabled = true;
button2.Enabled = true;

textBox1.Text = "finished:" + k + " " +
AppDomain.GetCurrentThreadId();

if (exc != null)
throw exc;
}

private void button1_Click(object sender, System.EventArgs e)
{
workerThreadDelegate = new WorkerThreadDelegate( WorkerThread );
iarMe = workerThreadDelegate.BeginInvoke( 100, null, null );

button1.Enabled = false;
button2.Enabled = false;
}

private void button2_Click(object sender, System.EventArgs e)
{
workerThreadDelegate = new WorkerThreadDelegate( WorkerThread );
iarMe = workerThreadDelegate.BeginInvoke( 0, null, null );
button1.Enabled = false;
button2.Enabled = false;
}

POLLING IS EVIL!