87

Sometimes the event pattern is used to raise events in MVVM applications by or a child viewmodel to send a message to its parent viewmodel in a loosely coupled way like this.

Parent ViewModel

searchWidgetViewModel.SearchRequest += (s,e) => 
{
    SearchOrders(searchWidgitViewModel.SearchCriteria);
};

SearchWidget ViewModel

public event EventHandler SearchRequest;

SearchCommand = new RelayCommand(() => {

    IsSearching = true;
    if (SearchRequest != null) 
    {
        SearchRequest(this, EventArgs.Empty);
    }
    IsSearching = false;
});

In refactoring my application for .NET4.5 I am making as much as code possible to use async and await. However the following doesn't work (well I really wasn't expecting it to)

 await SearchRequest(this, EventArgs.Empty);

The framework definitely does this to call event handlers such as this, but I'm not sure how it does it?

private async void button1_Click(object sender, RoutedEventArgs e)
{
   textBlock1.Text = "Click Started";
   await DoWork();
   textBlock2.Text = "Click Finished";
}

Anything I've found on the subject of raising events asynchrously is ancient but I can't find something in the framework to support this.

How can I await the calling of an event but remain on the UI thread.

4
  • What do you mean "doesn't work"? Sep 16, 2012 at 23:44
  • 1
    it doesn't compile. you can't await something that returns void which this does Sep 16, 2012 at 23:58
  • Do you really need to wait until all the handlers complete? Can't you just start them and let their async parts complete without waiting for them?
    – svick
    Sep 17, 2012 at 5:32
  • 1
    Microsoft now provides an AsyncEventHandler in Microsoft.VisualStudio.Threading package Sep 18, 2021 at 22:02

13 Answers 13

54

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.

9
  • 8
    Pretty good idea, but unfortunately the await is completed when any handler completes. That is, it does not wait for all tasks to complete.
    – Laith
    Feb 9, 2015 at 23:23
  • 3
    @Laith - I've double checked and it does seem to be working for me (I put debug WriteLine commands before and after the await call and the completed message didn't appear until after the search completed). did you use async on your event handler (added to answer) - and are you using multiple subscribers (untested) Feb 10, 2015 at 5:58
  • 2
    Yes, I'm talking about multiple subscribers. If you use += multiple times, the first Task to finish ends the await statement. The other tasks still continue, but are not awaited.
    – Laith
    Feb 10, 2015 at 6:08
  • 6
    [stackoverflow.com/a/3325424/429091](this answer about events returning values) suggests that the await would see the Task returned by the last handler to execute. Discussed in that answer’s comments, you may use Delegate.GetInvocationList() to manually execute the handlers individually and wrap their return in a Task.WhenAll().
    – binki
    Feb 8, 2016 at 21:35
  • 2
    Works well for single subscriber. Thanks!
    – str8ball
    Jun 7, 2022 at 22:43
40

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);
6
  • 1
    Very intelligent
    – bboyle1234
    Mar 16, 2018 at 9:32
  • await SearchRequest?.InvokeAsync(...) -- unfortunately, this doesn't work. We can't await null.
    – tsul
    Jul 2, 2018 at 12:07
  • 2
    You're right, when I wrote this c# 6 was not released yet and I didn't know exactly how the semantics would work: you should be able to write await (SearchRequest?.InvokeAsync(..) ?? Task.CompletedTask) though, not sure if it's actually more convenient.
    – tzachs
    Jul 2, 2018 at 14:04
  • Very good solution, i'd opt for enforcing the usage of a public constructor though in case the thread safety issue becomes a problem
    – Shikyo
    Feb 27, 2020 at 17:22
  • 2
    @ed22 the "+" operator is considered a setter so you'll need a public setter on the property and then it will work, see example: dotnetfiddle.net/NdWeLq. If you don't want a public setter you can replace the "+" and "-" operators with normal methods (i.e Subscribe and Unsubscribe).
    – tzachs
    Oct 19, 2021 at 12:50
33

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.

1
  • the link to your AsyncEx library is now broken Nov 11, 2022 at 9:00
12

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 Tasks back to the invoker. By making this part of the delegate’s signature, it is clearer that the delegate will be awaited.

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);
2
  • Easy to implement, easy to use. Amazing!
    – pschill
    Feb 15, 2021 at 14:46
  • thank you so much for this elegant solution. i'm using it here in my mongodb library if anyone wants to see an example. Jul 9, 2021 at 7:36
9

You can use the AsyncEventHandler delegate from the Microsoft.VisualStudio.Threading package provided by Microsoft and is used in Visual Studio from what I understand.

private AsyncEventHandler _asyncEventHandler;
_asyncEventHandler += DoStuffAsync;

Debug.WriteLine("Async invoke incoming!");
await _asyncEventHandler.InvokeAsync(this, EventArgs.Empty);
Debug.WriteLine("Done.");
private async Task DoStuffAsync(object sender, EventArgs args)
{
    await Task.Delay(1000);
    Debug.WriteLine("hello from async event handler");
    await Task.Delay(1000);
}

Output:
Async invoke incoming!
hello from async event handler
Done.

1
  • This is by far the best solution! Jan 23, 2023 at 17:26
5

I know it is an old question, but my best solution was in using TaskCompletionSource.

See the code:

var tcs = new TaskCompletionSource<object>();
service.loginCreateCompleted += (object sender, EventArgs e) =>
{
    tcs.TrySetResult(e.Result);
};
await tcs.Task;
1
  • This is good if you only need to await the first callback of an event. It would probably be a good idea to remove the event handler after the await. Jun 9, 2021 at 22:31
4

If you are using custom event handlers, you might want to take a look at the DeferredEvents, as it will allow you to raise and await for the handlers of an event, like this:

await MyEvent.InvokeAsync(sender, DeferredEventArgs.Empty);

The event handler will do something like this:

public async void OnMyEvent(object sender, DeferredEventArgs e)
{
    var deferral = e.GetDeferral();

    await DoSomethingAsync();

    deferral.Complete();
}

Alternatively, you can use the using pattern like this:

public async void OnMyEvent(object sender, DeferredEventArgs e)
{
    using (e.GetDeferral())
    {
        await DoSomethingAsync();
    }
}

You can read about the DeferredEvents here.

1
  • Interesting approach, takes a different angle than some other solutions. I still personally prefer to have the handler return Task and extract it by calling GetInvocationList(). For your usage example in this SO answer, I think it would be better to use the using (e.GetDeferral()) {} pattern variation because it’s less fragile.
    – binki
    Aug 23, 2018 at 19:38
2

I'm not clear on what you mean by "How can I await the calling of an event but remain on the UI thread". Do you want the event handler to be executed on the UI thread? If that's the case then you can do something like this:

var h = SomeEvent;
if (h != null)
{
    await Task.Factory.StartNew(() => h(this, EventArgs.Empty),
        Task.Factory.CancellationToken,
        Task.Factory.CreationOptions,
        TaskScheduler.FromCurrentSynchronizationContext());
}

Which wraps the invocation of the handler in a Task object so that you can use await, since you can't use await with a void method--which is where your compile error stems from.

But, I'm not sure what benefit you expect to get out of that.

I think there's a fundamental design issue there. It's fine to kick of some background work on a click event and you can implement something that supports await. But, what's the effect on how the UI can be used? e.g. if you have a Click handler that kicks off an operation that takes 2 seconds, do you want the user to be able to click that button while the operation is pending? Cancellation and timeout are additional complexities. I think much more understanding of the usability aspects needs to be done here.

6
  • in the example above I put 'IsSearching=true' and 'IsSearching=false' around the call. These are MVVM properties that would need to execute on the UI thread, prehaps to gray out the 'Search' button. Ultimately my question is that WPF (the Dispatcher?) can call a void event handler asynchronously like the 'button1_Click' event above and I want to know how they do that Sep 17, 2012 at 0:09
  • @Simon_Weaver Well, you can't await a method that returns void, you can only await a method that returns Task<T> or Task. So, if you want to invoke an event handler asynchronously, you have to wrap it in a Task object, which I showed in my answer. Sep 17, 2012 at 0:43
  • so do you think this is what WPF does when it calls my event handlers that are declared to be async? Sep 17, 2012 at 1:12
  • The async modifier just tells the compiler to generate an async state machine to manage any await keywords it encounters within the method (more to manage the lines after the awaits). It doesn't do anything as far as externally viewing the method. i.e., it doesn't make the method usable by await. await only depends on the method returning a Task variant. So, WPF is probably just calling your method synchronously. To call it asynchronously with await it would have to do what I detailed in my answer: wrap it in a Task object. Sep 17, 2012 at 2:21
  • You can certainly write an event handler that returns Task, but nothing else is going to call it asynchronously, directly. Sep 17, 2012 at 2:22
2

Since delegates (and events are delegates) implement the Asynchronous Programming Model (APM), you could use the TaskFactory.FromAsync method. (See also Tasks and the Asynchronous Programming Model (APM).)

public event EventHandler SearchRequest;

public async Task SearchCommandAsync()
{
    IsSearching = true;
    if (SearchRequest != null)
    {
        await Task.Factory.FromAsync(SearchRequest.BeginInvoke, SearchRequest.EndInvoke, this, EventArgs.Empty, null);
    }
    IsSearching = false;
}

The above code, however, will invoke the event on a thread pool thread, i.e. it will not capture the current synchronization context. If this is a problem, you could modify it as follows:

public event EventHandler SearchRequest;

private delegate void OnSearchRequestDelegate(SynchronizationContext context);

private void OnSearchRequest(SynchronizationContext context)
{
    context.Send(state => SearchRequest(this, EventArgs.Empty), null);
}

public async Task SearchCommandAsync()
{
    IsSearching = true;
    if (SearchRequest != null)
    {
        var search = new OnSearchRequestDelegate(OnSearchRequest);
        await Task.Factory.FromAsync(search.BeginInvoke, search.EndInvoke, SynchronizationContext.Current, null);
    }
    IsSearching = false;
}
1

To continue on Simon Weaver's answer, I tried the following

        if (SearchRequest != null)
        {
            foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
            {
                await onSearchRequest(null, EventArgs.Empty);
            }
        }

This seams to do the trick.

1
public static class FileProcessEventHandlerExtensions
{
    public static Task InvokeAsync(this FileProcessEventHandler handler, object sender, FileProcessStatusEventArgs args)
     => Task.WhenAll(handler.GetInvocationList()
                            .Cast<FileProcessEventHandler>()
                            .Select(h => h(sender, args))
                            .ToArray());
}
0

This is a little bit derivitive from @Simon_Weaver answer, but I find it useful. presume you have some class RaisesEvents which has an event RaisesEvents.MyEvent and you've got it injected to class MyClass, where you want to subscribe to MyEvent Probably better to do the subscribing in an Initialize() method, but for simplicity's sake:

public class MyClass
{
    public MyClass(RaisesEvent otherClass)
    {
        otherClass.MyEvent += MyAction;
    }

    private Action MyAction => async () => await ThingThatReturnsATask();

    public void Dispose() //it doesn't have to be IDisposable, but you should unsub at some point
    {
        otherClass.MyEvent -= MyAction;
    }

    private async Task ThingThatReturnsATask()
    {
        //async-await stuff in here
    }
}
0

Based on the Microsoft.VisualStudio.Threading library mentioned by @ShahinDohan, I wrote a minimal excerpt which ensures that calls to the event delegates are serialized/awaited and that possible exceptions are collected.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncEvents
{
    /// <summary>An asynchronous event handler.
    /// To trigger the event, use the function <see cref="AsyncEventHandlerExtensions.InvokeAsync{TEventArgs}(AsyncEventHandler{TEventArgs}, object, TEventArgs)"/>
    /// instead of <see cref="EventHandler.Invoke(object, EventArgs)"/>, it will guarantee a serialization
    /// of calls to the event delegates and collects any exceptions.
    /// <example><code>
    ///     public event AsyncEventHandler<EventArgs> MyEvent;
    /// 
    ///     // Trigger the event
    ///     public async Task OnMyEvent()
    ///     {
    ///         // A null check is not necessary, the ? operator would fail here
    ///         await MyEvent.InvokeIfNotNullAsync(this, new EventArgs());
    ///     }
    /// </code></example>
    /// </summary>
    /// <typeparam name="TEventArgs">The type of event arguments.</typeparam>
    /// <param name="sender">The sender of the event.</param>
    /// <param name="args">Event arguments.</param>
    /// <returns>An awaitable task.</returns>
    public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs args);

    internal static class AsyncEventHandlerExtensions
    {
        /// <summary>
        /// Invokes asynchronous event handlers, returning an awaitable task. Each handler is fully executed
        /// before the next handler in the list is invoked.
        /// </summary>
        /// <typeparam name="TEventArgs">The type of argument passed to each handler.</typeparam>
        /// <param name="handlers">The event handlers. May be <c>null</c>.</param>
        /// <param name="sender">The event source.</param>
        /// <param name="args">The event argument.</param>
        /// <returns>An awaitable task that completes when all handlers have completed.</returns>
        /// <exception cref="T:System.AggregateException">Thrown if any handlers fail. It contains all
        /// collected exceptions.</exception>
        public static async Task InvokeIfNotNullAsync<TEventArgs>(
            this AsyncEventHandler<TEventArgs> handlers,
            object sender,
            TEventArgs args)
        {
            if (handlers == null)
                return;

            List<Exception> exceptions = null;
            Delegate[] listenerDelegates = handlers.GetInvocationList();
            for (int index = 0; index < listenerDelegates.Length; ++index)
            {
                var listenerDelegate = (AsyncEventHandler<TEventArgs>)listenerDelegates[index];
                try
                {
                    await listenerDelegate(sender, args).ConfigureAwait(true);
                }
                catch (Exception ex)
                {
                    if (exceptions == null)
                        exceptions = new List<Exception>(2);
                    exceptions.Add(ex);
                }
            }

            // Throw collected exceptions, if any
            if (exceptions != null)
                throw new AggregateException(exceptions);
        }
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.