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:
- The event handler delegate gets unregistered after the event fires. Otherwise the delegate would remain in memory as long as
Something
did.
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.
async void
. This is the only case whereasync void
should be used – ArnaudTaskCompletionSource
to fire a method; and wait for an event; toawait
it – BoundedTaskCompletionSource
. If, as I suspect, you want to await multiple events you'll needIAsyncEnumerable<T>
. I posted an answer that shows how to do both – Arnaud