Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async await and event handler

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(...);
   }
}
like image 395
Mselmi Ali Avatar asked May 08 '26 10:05

Mselmi Ali


2 Answers

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.

like image 192
Panagiotis Kanavos Avatar answered May 10 '26 23:05

Panagiotis Kanavos


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.

like image 34
Theodor Zoulias Avatar answered May 11 '26 00:05

Theodor Zoulias



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!