To answer the direct question: I do not think EventHandler
allows implementations to communicate sufficiently back to the invoker to allow proper awaiting. You might be able to perform tricks with a custom synchronization context, but if you care about waiting for the handlers, it is better that the handlers are able to return their Task
s back to the invoker. By making this part of the delegate’s signature, it is clearer that the delegate will be await
ed.
I suggest using the Delgate.GetInvocationList()
approach described in Ariel’s answer mixed with ideas from tzachs’s answer. Define your own AsyncEventHandler<TEventArgs>
delegate which returns a Task
. Then use an extension method to hide the complexity of invoking it correctly. I think this pattern makes sense if you want to execute a bunch of asynchronous event handlers and wait for their results.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public delegate Task AsyncEventHandler<TEventArgs>(
object sender,
TEventArgs e)
where TEventArgs : EventArgs;
public static class AsyncEventHandlerExtensions
{
public static IEnumerable<AsyncEventHandler<TEventArgs>> GetHandlers<TEventArgs>(
this AsyncEventHandler<TEventArgs> handler)
where TEventArgs : EventArgs
=> handler.GetInvocationList().Cast<AsyncEventHandler<TEventArgs>>();
public static Task InvokeAllAsync<TEventArgs>(
this AsyncEventHandler<TEventArgs> handler,
object sender,
TEventArgs e)
where TEventArgs : EventArgs
=> Task.WhenAll(
handler.GetHandlers()
.Select(handleAsync => handleAsync(sender, e)));
}
This allows you to create a normal .net-style event
. Just subscribe to it as you normally would.
public event AsyncEventHandler<EventArgs> SomethingHappened;
public void SubscribeToMyOwnEventsForNoReason()
{
SomethingHappened += async (sender, e) =>
{
SomethingSynchronous();
// Safe to touch e here.
await SomethingAsynchronousAsync();
// No longer safe to touch e here (please understand
// SynchronizationContext well before trying fancy things).
SomeContinuation();
};
}
Then simply remember to use the extension methods to invoke the event rather than invoking them directly. If you want more control in your invocation, you may use the GetHandlers()
extension. For the more common case of waiting for all the handlers to complete, just use the convenience wrapper InvokeAllAsync()
. In many patterns, events either don’t produce anything the caller is interested in or they communicate back to the caller by modifying the passed in EventArgs
. (Note, if you can assume a synchronization context with dispatcher-style serialization, your event handlers may mutate the EventArgs
safely within their synchronous blocks because the continuations will be marshaled onto the dispatcher thread. This will magically happen for you if, for example, you invoke and await
the event from a UI thread in winforms or WPF. Otherwise, you may have to use locking when mutating EventArgs
in case if any of your mutations happen in a continuation which gets run on the threadpool).
public async Task Run(string[] args)
{
if (SomethingHappened != null)
await SomethingHappened.InvokeAllAsync(this, EventArgs.Empty);
}
This gets you closer to something that looks like a normal event invocation, except that you have to use .InvokeAllAsync()
. And, of course, you still have the normal issues that come with events such as needing to guard invocations for events with no subscribers to avoid a NullArgumentException
.
Note that I am not using await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty)
because await
explodes on null
. You could use the following call pattern if you want, but it can be argued that the parens are ugly and the if
style is generally better for various reasons:
await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
async
parts complete without waiting for them?AsyncEventHandler
in Microsoft.VisualStudio.Threading package