How to 'await' raising an EventHandler event
Events don't mesh perfectly with async
and await
, as you've discovered.
The way UIs handle async
events is different than what you're trying to do. The UI provides a SynchronizationContext
to its async
events, enabling them to resume on the UI thread. It does not ever "await" them.
Best Solution (IMO)
I think the best option is to build your own async
-friendly pub/sub system, using AsyncCountdownEvent
to know when all handlers have completed.
Lesser Solution #1
async void
methods do notify their SynchronizationContext
when they start and finish (by incrementing/decrementing the count of asynchronous operations). All UI SynchronizationContext
s ignore these notifications, but you could build a wrapper that tracks it and returns when the count is zero.
Here's an example, using AsyncContext
from my AsyncEx library:
SearchCommand = new RelayCommand(() => {
IsSearching = true;
if (SearchRequest != null)
{
AsyncContext.Run(() => SearchRequest(this, EventArgs.Empty));
}
IsSearching = false;
});
However, in this example the UI thread is not pumping messages while it's in Run
.
Lesser Solution #2
You could also make your own SynchronizationContext
based on a nested Dispatcher
frame that pops itself when the count of asynchronous operations reaches zero. However, you then introduce re-entrancy problems; DoEvents
was left out of WPF on purpose.
Based on Simon_Weaver's answer, I created a helper class that can handle multiple subscribers, and has a similar syntax to c# events.
public class AsyncEvent<TEventArgs> where TEventArgs : EventArgs
{
private readonly List<Func<object, TEventArgs, Task>> invocationList;
private readonly object locker;
private AsyncEvent()
{
invocationList = new List<Func<object, TEventArgs, Task>>();
locker = new object();
}
public static AsyncEvent<TEventArgs> operator +(
AsyncEvent<TEventArgs> e, Func<object, TEventArgs, Task> callback)
{
if (callback == null) throw new NullReferenceException("callback is null");
//Note: Thread safety issue- if two threads register to the same event (on the first time, i.e when it is null)
//they could get a different instance, so whoever was first will be overridden.
//A solution for that would be to switch to a public constructor and use it, but then we'll 'lose' the similar syntax to c# events
if (e == null) e = new AsyncEvent<TEventArgs>();
lock (e.locker)
{
e.invocationList.Add(callback);
}
return e;
}
public static AsyncEvent<TEventArgs> operator -(
AsyncEvent<TEventArgs> e, Func<object, TEventArgs, Task> callback)
{
if (callback == null) throw new NullReferenceException("callback is null");
if (e == null) return null;
lock (e.locker)
{
e.invocationList.Remove(callback);
}
return e;
}
public async Task InvokeAsync(object sender, TEventArgs eventArgs)
{
List<Func<object, TEventArgs, Task>> tmpInvocationList;
lock (locker)
{
tmpInvocationList = new List<Func<object, TEventArgs, Task>>(invocationList);
}
foreach (var callback in tmpInvocationList)
{
//Assuming we want a serial invocation, for a parallel invocation we can use Task.WhenAll instead
await callback(sender, eventArgs);
}
}
}
To use it, you declare it in your class, for example:
public AsyncEvent<EventArgs> SearchRequest;
To subscribe an event handler, you'll use the familiar syntax (the same as in Simon_Weaver's answer):
myViewModel.SearchRequest += async (s, e) =>
{
await SearchOrders();
};
To invoke the event, use the same pattern we use for c# events (only with InvokeAsync):
var eventTmp = SearchRequest;
if (eventTmp != null)
{
await eventTmp.InvokeAsync(sender, eventArgs);
}
If using c# 6, one should be able to use the null conditional operator and write this instead:
await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
Edit: This doesn't work well for multiple subscribers, so unless you only have one I wouldn't recommend using this.
Feels slightly hacky - but I have never found anything better:
Declare a delegate. This is identical to EventHandler
but returns a task instead of void
public delegate Task AsyncEventHandler(object sender, EventArgs e);
You can then run the following and as long as the handler declared in the parent uses async
and await
properly then this will run asynchronously:
if (SearchRequest != null)
{
Debug.WriteLine("Starting...");
await SearchRequest(this, EventArgs.Empty);
Debug.WriteLine("Completed");
}
Sample handler:
// declare handler for search request
myViewModel.SearchRequest += async (s, e) =>
{
await SearchOrders();
};
Note: I've never tested this with multiple subscribers and not sure how this will work - so if you need multiple subscribers then make sure to test it carefully.