Reputation: 49
I have upgraded a .NET 4.0 WinForms program to .NET 4.5.1 in the hope of using the new await on async WCF calls in order to prevent freezing the UI while waiting for data (the original was quickly written so I was hoping the old synchronous WCF calls could be made async with minimal change to existing code using the new await feature).
From what I understand, await was supposed to return to the UI thread with no extra coding, but for some reason it does not for me, so the following would give the cross thread exception:
private async void button_Click(object sender, EventArgs e)
{
using (MyService.MyWCFClient myClient = MyServiceConnectFactory.GetForUser())
{
var list=await myClient.GetListAsync();
dataGrid.DataSource=list; // fails if not on UI thread
}
}
Following the article await anything I made a custom awaiter so I could issue an await this
to get back on the UI thread, which solved the exception, but then I found my UI was still frozen despite using the asynchronously Tasks generated by Visual Studio 2013 for my WCF service.
Now the program is actually a Hydra VisualPlugin running inside an old Delphi application, so if anything could mess things up, is probably happens... But does anybody have any experience with what exactly could make awaiting an async WCF not returning to the UI thread or hang the UI thread? Maybe upgrading from 4.0 to 4.5.1 makes the program miss some reference to do the magic?
Now while I would like to understand why await does not work as advertised, I ended up with making my own workaround: A custom awaiter that forces a task to run in a background thread, and which forces the continuation to arrive back on the UI thread.
Similarly to .ConfigureAwait(false)
I wrote an .RunWithReturnToUIThread(this)
extension for Taks as follows:
public static RunWithReturnToUIThreadAwaiter<T> RunWithReturnToUIThread<T>(this Task<T> task, Control control)
{
return new RunWithReturnToUIThreadAwaiter<T>(task, control);
}
public class RunWithReturnToUIThreadAwaiter<T> : INotifyCompletion
{
private readonly Control m_control;
private readonly Task<T> m_task;
private T m_result;
private bool m_hasResult=false;
private ExceptionDispatchInfo m_ex=null; // Exception
public RunWithReturnToUIThreadAwaiter(Task<T> task, Control control)
{
if (task == null) throw new ArgumentNullException("task");
if (control == null) throw new ArgumentNullException("control");
m_task = task;
m_control = control;
}
public RunWithReturnToUIThreadAwaiter<T> GetAwaiter() { return this; }
public bool IsCompleted
{
get
{
return !m_control.InvokeRequired && m_task.IsCompleted; // never skip the OnCompleted event if invoke is required to get back on UI thread
}
}
public void OnCompleted(Action continuation)
{
// note to self: OnCompleted is not an event - it is called to specify WHAT should be continued with ONCE the result is ready, so this would be the place to launch stuff async that ends with doing "continuation":
Task.Run(async () =>
{
try
{
m_result = await m_task.ConfigureAwait(false); // await doing the actual work
m_hasResult = true;
}
catch (Exception ex)
{
m_ex = ExceptionDispatchInfo.Capture(ex); // remember exception
}
finally
{
m_control.BeginInvoke(continuation); // give control back to continue on UI thread even if ended in exception
}
});
}
public T GetResult()
{
if (m_ex == null)
{
if (m_hasResult)
return m_result;
else
return m_task.Result; // if IsCompleted returned true then OnCompleted was never run, so get the result here
}
else
{ // if ended in exception, rethrow it
m_ex.Throw();
throw m_ex.SourceException; // just to avoid compiler warning - the above does the work
}
}
}
Now in the above I am not sure if my exception handling is needed like this, or if the Task.Run really need to use async and await in its code, or if the multiple layers of Tasks could give problems (I am basically bypassing the encapsulated Task's own method of returning - since it did not return correctly in my program for WCF services).
Any comments/ideas regarding efficiency of the above workaround, or what caused the problems to begin with?
Upvotes: 0
Views: 1303
Reputation: 456717
Now the program is actually a Hydra VisualPlugin running inside an old Delphi application
That's probably the problem. As I explain in my async
intro blog post, when you await
an Task
and that task is incomplete, the await
operator by default will capture a "current context" and later resume the async
method in that context. The "current context" is SynchronizationContext.Current
unless it is null
, in which case it is TaskScheduler.Current
.
So, the normal "return to UI thread" behavior is a result of await
capturing the UI synchronization context - in the case of WinForms, a WinFormsSynchronizationContext
.
In a normal WinForms application, SynchronizationContext.Current
is set to a WinFormsSynchronizationContext
the first time you create a Control
. Unfortunately, this does not always happen in plugin architectures (I've seen similar behavior on Microsoft Office plugins). I suspect that when your code awaits, SynchronizationContext.Current
is null
and TaskScheduler.Current
is TaskScheduler.Default
(i.e., the thread pool task scheduler).
So, the first thing I'd try is creating a Control
:
void EnsureProperSynchronizationContext()
{
if (SynchronizationContext.Current == null)
var _ = new Control();
}
Hopefully, you would only have to do that once, when your plugin is first invoked. But you may have to do it at the beginning of all your methods that can be invoked by the host.
If that doesn't work, you can create your own SynchronizationContext
, but it's best to use the WinForms one if you can. A custom awaiter is also possible (and if you go that route, it's easier to wrap TaskAwaiter<T>
rather than Task<T>
), but the disadvantage of a custom awaiter is that is has to go on every await
.
Upvotes: 2