Async await and event handler
Asked Answered
T

3

5

Is it permitted to convert a usual event handler from void to Task based, and await it like below?

Something.PropertyChanged += async (o, args) => await IsButtonVisible_PropertyChanged(o, args);  
Something.PropertyChanged -= async (o, args) => await IsButtonVisible_PropertyChanged(o, args);  

private Task IsButtonVisible_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
   if (IsSomthingEnabled)
   {
       return SomeService.ExecuteAsync(...);
   }

   return Task.CompletedTask;
}

Or do it like this?

Something.PropertyChanged += IsButtonVisible_PropertyChanged;  
Something.PropertyChanged -= IsButtonVisible_PropertyChanged;  

private void IsButtonVisible_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
   if (IsSomthingEnabled)
   {
       _ = SomeService.ExecuteAsync(...);
   }
}

Update: Or this one, I know that the use of async void should be banned, because exception is not caught, but maybe for the case of an event handler it's OK since the event handler doesn't return.

Something.PropertyChanged += IsButtonVisible_PropertyChanged;  
Something.PropertyChanged -= IsButtonVisible_PropertyChanged;  

private async void IsButtonVisible_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
   if (IsSomthingEnabled)
   {
       await = SomeService.ExecuteAsync(...);
   }
}
Trinity answered 27/8, 2021 at 10:26 Comment(8)
event handlers don't return so there is nothing to wait for, if you need a response from a event then you should create an event arg that has a await-able value, (most likely an even in its own right)Idolism
it make sense for me, thanks.Trinity
The correct syntax is the second with async void. This is the only case where async void should be usedArnaud
You might be looking for a TaskCompletionSource to fire a method; and wait for an event; to await itBounded
Are you asking how to write an asynchronous event handler or how to await an event?Arnaud
@PanagiotisKanavos, I'm asking how to properly propagate and await my async method which is called inside an event handler, should I use discard operator or return a task and await it like in the first exp.Trinity
you don't need the = between await and the item being awaitedIdolism
@MselmiAli you can await a single event with a TaskCompletionSource. If, as I suspect, you want to await multiple events you'll need IAsyncEnumerable<T>. I posted an answer that shows how to do bothArnaud
A
14

The syntax for asynchronous event handlers is :

Something.PropertyChanged += IsButtonVisible_PropertyChanged;  
... 

private async void IsButtonVisible_PropertyChanged(object sender,
                                                   PropertyChangedEventArgs e)
{
   if (IsSomethingEnabled)
   {
       await SomeService.ExecuteAsync(...);
   }
}

This allows awaiting asynchronous operations inside the event handler without blocking the UI thread. This can't be used to await for an event in some other method though.

Awaiting a single event

If you want some other code to await for an event to complete you need a TaskCompletionSource. This is explained in Tasks and the Event-based Asynchronous Pattern (EAP).

public Task<string> OnPropChangeAsync(Something x)
{
     var options=TaskCreationOptions.RunContinuationsAsynchronously;
     var tcs = new TaskCompletionSource<string>(options);
     x.OnPropertyChanged += onChanged;
     return tcs.Task;

     void onChanged(object sender,PropertyChangedEventArgs e)
     {
         tcs.TrySetResult(e.PropertyName);
         x.OnPropertyChanged -= onChanged;
     }
     
}

....

async Task MyAsyncMethod()
{
    var sth=new Something();
    ....
    var propName=await OnPropertyChangeAsync(sth);
   
    if (propName=="Enabled" && IsSomethingEnabled)
    {
        await SomeService.ExecuteAsync(...);
    }

}

This differs from the example in two places:

  1. The event handler delegate gets unregistered after the event fires. Otherwise the delegate would remain in memory as long as Something did.
  2. TaskCreationOptions.RunContinuationsAsynchronously ensures that any continuations will run on a separate thread. The default is to run them on the same thread that sets the result

This method will await only a single event. Calling it in a loop will create a new TCS each time, which is wasteful.

Awaiting a stream of events

It wasn't possible to easily await multiple events until IAsyncEnumerable was introduced in C# 8. With IAsyncEnumerable<T> and Channel, it's possible to create a method that will send a stream of notifications :

public IAsyncEnumerable<string> OnPropChangeAsync(Something x,CancellationToken token)
{
     var channel=Channel.CreateUnbounded<string>();
     //Finish on cancellation
     token.Register(()=>channel.Writer.TryComplete());
     x.OnPropertyChanged += onChanged;
     
     return channel.Reader.ReadAllAsync();

     async void onChanged(object sender,PropertyChangedEventArgs e)
     {
         channel.Writer.SendAsync(e.PropertyName);
     }
     
}

....

async Task MyAsyncMethod(CancellationToken token)
{  
    var sth=new Something();
    ....
    await foreach(var prop in OnPropertyChangeAsync(sth),token)
    {
   
        if (propName=="Enabled" && IsSomethingEnabled)
        {
           await SomeService.ExecuteAsync(...);
        }
    }

}

In this case, only one event handler is needed. Every time an event occurs the property named is pushed to the Channel. Channel.Reader.ReadAllAsync() is used to return an IAsyncEnumerable<string> that can be used to loop asynchronously. The loop will keep running until the CancellationToken is signaled, in which case the writer will go into the Completed state and the IAsyncEnumerable<T> will terminate.

Arnaud answered 27/8, 2021 at 11:24 Comment(1)
Plus 1, always great answers and covered all basesBounded
S
6

Quoting from Microsoft's article Async/Await - Best Practices in Asynchronous Programming, and specifically from the Avoid async void section:

Void-returning async methods have a specific purpose: to make asynchronous event handlers possible. [...] Event handlers naturally return void, so async methods return void so that you can have an asynchronous event handler.

Based on this, your third approach is the correct one:

private async void IsButtonVisible_PropertyChanged(object sender,
    PropertyChangedEventArgs e)
{
   if (IsSomethingEnabled)
   {
       await SomeService.ExecuteAsync();
   }
}

Your first approach (+= async (o, args) => await) is technically equivalent, but it's not recommended because it is idiomatic and may cause confusion to future maintainers.

Your second approach (_ = SomeService.ExecuteAsync() launches the asynchronous operation in a fire-and-forget fashion, which is rarely a good idea because your application completely loses track of this task. It also elides async and await, which opens another can of worms.

Sweeten answered 27/8, 2021 at 16:45 Comment(0)
I
1

the syntax for an async Event Handler is

async void handler(object sender,EventArgs args){}

and as Events don't return there is nothing to await for, so waiting for them is pointless

however if you need a response from an event then you can use the EventsArgs class to provide the response, eg

class FeedbackEventArgs:EventArgs
{
    event EventHandler Completed;
    Complete(){
        this.Completed(this,EventArgs.Empty);
    }
}

then you can use it as

event EventHandler<FeedbackEventArgs> myFeedbackEvent;

args = new FeedbackEventArgs();
args.Completed += OnCompleted;
this.myFeedbackEvent(this,args)

note if your handler is not async then you can assume that you code was paused while the event occurred, in which case you can just read a property from the eventArg rather than having to trigger an event

class FeedbackEventArgs:EventArgs
{
    int result{get;set;}
}

event EventHandler<FeedbackEventArgs> myFeedbackEvent;

this.myFeedbackEvent(this,args)
args.result //this will be the result set in the sync handler

as noted by @Panagiotis this is a conceptual example not a working example

Idolism answered 27/8, 2021 at 10:47 Comment(2)
The first part is correct, the second isn't. While that code can run (with several fixes), everything runs on the main thread. To actually await an event you need a TaskCompletionSourceArnaud
you don't need as TaskCompletionSource , however that is a simple way to avoid cross threading issuesIdolism

© 2022 - 2024 — McMap. All rights reserved.