I recently updated some C# code to use the async/await feature to offload some processing to a background thread. Mine is a WPF GUI application using .NET Framework 4.6.1.
A problem I immediately ran into was this: my background thread calls back into the UI to show status updates. But, when that call was made to show a line of status text, the app caused an unhandled exception (System.InvalidOperationException). I realized immediately what the problem was, having dealt with it many times in other multithreaded applications. You cannot update user interface controls from secondary threads. I knew the answer for C++ wxWidgets applications, but had to figure out what to do for C# and WPF.
I, of course, did a lot of digging online and got sidetracked with likely-looking solutions, which then turned out to be for WinForms applications. Ultimately, I found that the answer lies with the System.Windows.Threading.Dispatcher class. The documentation states it "Provides services for managing the queue of work items for a thread." Sounds like just what I needed.
In my method that outputs the status line of text, it appends the new text to a TextBlock control. Before doing that, though, the method now needed to check whether it was running in a background thread, or the primary thread of the UI. Here is the new method I wrote to determine this:
/// <summary>
/// Determine if current thread is secondary or primary (which
/// is determined from this object's dispatcher)
/// </summary>
protected bool IsBackgroundThread()
{
return (this.Dispatcher != Dispatcher.CurrentDispatcher);
}
This IsBackgroundThread method is implemented in the main window class derived from System.Windows.Window, so is a class created with the application primary thread's dispatcher. Thus, "this.Dispatcher" returns that primary thread Dispatcher object. The static property "Dispatcher.CurrentDispatcher" returns the Dispatcher object for the currently running thread. So, comparing the two thus determines background versus primary thread.Now that I had the ability to determine the thread context, I could use it when updating the UI control with the new status text. I did this in an AppendOutput method.
/// <summary>
/// Append the given text string to the control that shows
/// operation results.
/// </summary>
/// <param name="text" />The new text to append.
protected void AppendOutput(string text)
{
if (IsBackgroundThread())
{
// Method was called from secondary thread, so dispatch
// again to the primary thread using it's dispatcher
this.Dispatcher.Invoke(() => AppendOutput(text),
DispatcherPriority.Normal);
}
else
{
// Method was called from primary thread, so do the work here
textBlockResult.Text = textBlockResult.Text + "\n" + text;
}
}
The above is a generalized way to determine the primary thread on a particular window, but the control on that window can be queried, too. In this simpler method, the code merely calls the DispatcherObject.CheckAccess method, as follows:
if (!textBlockResult.CheckAccess())
{
this.Dispatcher.Invoke(() => AppendOutput(text),
DispatcherPriority.Normal);
}
When executing in the primary thread, the given text string is simply appended to the TextBlock control's current text value as a new line. When running in a background thread, the primary thread's Dispatcher is called to invoke the given Action, which is in the form of a Lambda expression that simply recursively calls the same method, but now in the primary thread's context.Note that the Invoke method is of the form Invoke(Action, DispatcherPriority), where the first argument implicitly creates an Action delegate object for the method call. The documentation states, for Invoke, "Executes the specified Action synchronously at the specified priority on the thread the Dispatcher is associated with." They key here is this is a synchronous call in a different thread context.
And that's it. With this change, all the status messages from the background thread show up just fine in the TextBlock control.
No comments:
Post a Comment