I\'m trying to create a control that exposes a DoLoading
event that consumers can subscribe to in order to perform loading operations. For convenience, event ha
First off, I recommend you reconsider the design of your "asynchronous event".
It is true that you can use a return value of Task
, but it's more natural for C# event handlers to return void
. In particular, if you have multiple subscriptions, the Task
returned from handler(this, ...)
is only the return value of one of the event handlers. To properly wait for all async events to complete, you'd need to use Delegate.GetInvocationList
with Task.WhenAll
when you raise the event.
Since you're already on the WinRT platform, I recommend you use "deferrals". This is the solution chosen by the WinRT team for asynchronous events, so it should be familiar to consumers of your class.
Unfortunately, the WinRT team did not include the deferral infrastructure in the .NET framework for WinRT. So I wrote a blog post about async event handlers and how to build a deferral manager.
Using a deferral, your event-raising code would look like this:
private Task OnDoLoading(bool mustLoadPreviousRunningState)
{
var handler = this.DoLoading;
if (handler == null)
return;
var args = new DoLoadingEventArgs(this, mustLoadPreviousRunningState);
handler(args);
return args.WaitForDeferralsAsync();
}
private Task PerformLoadingActionAsync()
{
TaskFactory uiFactory = new TaskFactory(_uiScheduler);
// Trigger event on the UI thread.
var eventHandlerTask = uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState)).Unwrap();
Task commandTask = Task.Run(() => this.ExecuteCommand());
return Task.WhenAll(eventHandlerTask, commandTask);
}
So that's my recommendation for a solution. The benefits of a deferral are that it enables both synchronous and asynchronous handlers, it's a technique already familiar to WinRT developers, and it correctly handles multiple subscribers without additional code.
Now, as to why the original code doesn't work, you can think this through by paying careful attention to all the types in your code and identifying what each task represents. Keep in mind the following important points:
Task<T>
derives from Task
. This means Task<Task>
will convert down to Task
without any warnings.StartNew
is not async
-aware so it behaves differently than Task.Run
. See Stephen Toub's excellent blog post on the subject.Your OnDoLoading
method will return a Task
representing the completion of the last event handler. Any Task
s from other event handlers are ignored (as I mention above, you should use Delegate.GetInvocationList
or deferrals to properly support multiple asynchronous handlers).
Now let's look at PerformLoadingActionAsync
:
Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState));
There's a lot going on in this statement. It's semantically equivalent to this (slightly simpler) line of code:
Task evenHandlerTask = await uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState));
OK, so we are queueing up OnDoLoading
to the UI thread. The return type of OnDoLoading
is Task
, so the return type of StartNew
is Task<Task>
. Stephen Toub's blog goes into the details of this kind of wrapping, but you can think of it like this: the "outer" task represents the start of the asynchronous OnDoLoading
method (up until it has to yield at an await
), and the "inner" task represents the completion of the asynchronous OnDoLoading
method.
Next, we await
the result of StartNew
. This unwraps the "outer" task and we get a Task
that represents the completion of OnDoLoading
stored in evenHandlerTask
.
return Task.WhenAll(evenHandlerTask, commandTask);
Now you're returning a Task
that represents when both commandTask
and evenHandlerTask
have completed. However, you're in an async
method, so your actual return type is Task<Task>
- and it's the inner task that represents what you want. I think what you meant to do was:
await Task.WhenAll(evenHandlerTask, commandTask);
Which would give you a return type of Task
, representing the full completion.
If you look at how it's called:
this.PerformLoadingActionAsync().ContinueWith(...)
ContinueWith
is acting on the outer Task
in the original code, when you really wanted it to act on the inner Task
.
You correctly realized that StartNew()
returns Task<Task>
in this case, and you care about the inner Task
(though I'm not sure why are you waiting for the outer Task
before starting commandTask
).
But then you return Task<Task>
and ignore the inner Task
. What you should do is to use await
instead of return
and change the return type of PerformLoadingActionAsync()
to just Task
:
await Task.WhenAll(evenHandlerTask, commandTask);
Few more notes:
Using event handlers this way is quite dangerous, because you care about the Task
returned from the handler, but if there are more handlers, only the last Task
will be returned if you raise the event normally. If you really want to do this, you should call GetInvocationList(), which lets you invoke and await
each handler separately:
private async Task OnDoLoading(bool mustLoadPreviousRunningState)
{
var handler = this.DoLoading;
if (handler != null)
{
var handlers = handler.GetInvocationList();
foreach (AsyncEventHandler<bool> innerHandler in handlers)
{
await innerHandler(this, mustLoadPreviousRunningState);
}
}
}
If you know that you'll never have more than one handler, you could use a delegate property that can be directly set instead of an event.
If you have an async
method or lambda that has the only await
just before its return
(and no finally
s), then you don't need to make it async
, just return the Task
directly:
Task.Factory.StartNew(() => this.OnDoLoading(true))