When developing winform applications it is common that you will have to just invoke to get the main GUI thread to do the GUI work.
Invoke
are today obsolete(If I read correct), instead you are suppose to use the SynchronizationContext
instead.
The question is : How do I handle exceptions? I have notice that sometimes the exception thrown on the "synced/invoked" thread is lost?
I do use the Application.ThreadException
and AppDomain.CurrentDomain.UnhandledException
but this does not help?
First of all, Synchronization Context is nothing new, it's been there since .NET 2.0. It has nothing to do specifically with exception handling. Neither does it make Control.Invoke
obsolete. In fact, WinFormsSynchronizationContext
, which is the WinForms' implementation of synchronization context, uses Control.BeginInvoke
for Post
and Control.Invoke
for Send
methods.
How do I handle exceptions? I have notice that sometimes the exception thrown on the "synced/invoked" thread is lost?
There is a well-documented behavior behind "sometimes" here. Control.Invoke
is a synchronous call, which propagates exceptions from inside the callback to the calling thread:
int Test()
{
throw new InvalidOperationException("Surpise from the UI thread!");
}
void Form_Load(object sender, EventArgs e)
{
// UI thread
ThreadPool.QueueUserWorkItem(x =>
{
// pool thread
try
{
this.Invoke((MethodInvoker)Test);
}
catch (Exception ex)
{
Debug.Print(ex.Message);
}
});
}
The benefit of using SynchronizationContext
is in de-coupling the WinForms specifics. It makes sense for a portable library, which potentially may be used by WinForms, WPF, Windows Phone, Xamarin or any other client:
// UI thread
var uiSynchronizationContext = System.Threading.SynchronizationContext.Current;
if (uiSynchronizationContext == null)
throw new NullReferenceException("SynchronizationContext.Current");
ThreadPool.QueueUserWorkItem(x =>
{
// pool thread
try
{
uiSynchronizationContext.Send(s => Test(), null);
}
catch (Exception ex)
{
Debug.Print(ex.ToString());
}
});
Thus, with Control.Invoke
(or SynchronizationContext.Send
) you have an option to handle exceptions on the calling thread. You don't have such option with Control.BeginInvoke
(or SynchronizationContext.Post
), by design and by common sense. That's because Control.BeginInvoke
is asynchronous, it queues a callback to be executed upon a future iteration of the message loop run by Application.Run
.
To be able to handle exception thrown by an asynchronous callback, you'd need to actually observe the completion of the asynchronous operation. Before C# 5.0, you could have used events or Task.ContinueWith
for that.
Using an event:
class ErrorEventArgs : EventArgs
{
public Exception Exception { get; set; }
}
event EventHandler<ErrorEventArgs> Error = delegate { };
void Form_Load(object sender, EventArgs e)
{
this.Error += (sError, eError) =>
// handle the error on the UI thread
Debug.Print(eError.Exception.ToString());
ThreadPool.QueueUserWorkItem(x =>
{
this.BeginInvoke(new MethodInvoker(() =>
{
try
{
Test();
}
catch (Exception ex)
{
// fire the Error event
this.Error(this, new ErrorEventArgs { Exception = ex });
}
}));
});
}
Using ContinueWith
:
ThreadPool.QueueUserWorkItem(x =>
{
var tcs = new TaskCompletionSource<int>();
uiSynchronizationContext.Post(s =>
{
try
{
tcs.SetResult(Test());
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}, null);
// observe the completion,
// only if there's an error
tcs.Task.ContinueWith(task =>
{
// handle the error on a pool thread
Debug.Print(task.Exception.ToString());
}, TaskContinuationOptions.OnlyOnFaulted);
});
Finally, with C# 5.0, you can use async/await
and handle exceptions thrown asynchronously with the same convenience of try/catch
you have for synchronous calls:
int Test()
{
throw new InvalidOperationException("Surpise from the UI thread!");
}
async void Form_Load(object sender, EventArgs e)
{
// UI thread
var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
await Task.Run(async () =>
{
// pool thread
try
{
await Task.Factory.StartNew(
() => Test(),
CancellationToken.None,
TaskCreationOptions.None,
uiTaskScheduler);
}
catch (Exception ex)
{
// handle the error on a pool thread
Debug.Print(ex.ToString());
}
});
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With