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 SynchronizationContexts 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.