Is it possible to await an event instead of another async method?

92,064

Solution 1

You can use an instance of the SemaphoreSlim Class as a signal:

private SemaphoreSlim signal = new SemaphoreSlim(0, 1);

// set signal in event
signal.Release();

// wait for signal somewhere else
await signal.WaitAsync();

Alternatively, you can use an instance of the TaskCompletionSource<T> Class to create a Task<T> that represents the result of the button click:

private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

// complete task in event
tcs.SetResult(true);

// wait for task somewhere else
await tcs.Task;

Solution 2

When you have an unusual thing you need to await on, the easiest answer is often TaskCompletionSource (or some async-enabled primitive based on TaskCompletionSource).

In this case, your need is quite simple, so you can just use TaskCompletionSource directly:

private TaskCompletionSource<object> continueClicked;

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
  // Note: You probably want to disable this button while "in progress" so the
  //  user can't click it twice.
  await GetResults();
  // And re-enable the button here, possibly in a finally block.
}

private async Task GetResults()
{ 
  // Do lot of complex stuff that takes a long time
  // (e.g. contact some web services)

  // Wait for the user to click Continue.
  continueClicked = new TaskCompletionSource<object>();
  buttonContinue.Visibility = Visibility.Visible;
  await continueClicked.Task;
  buttonContinue.Visibility = Visibility.Collapsed;

  // More work...
}

private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
  if (continueClicked != null)
    continueClicked.TrySetResult(null);
}

Logically, TaskCompletionSource is like an async ManualResetEvent, except that you can only "set" the event once and the event can have a "result" (in this case, we're not using it, so we just set the result to null).

Solution 3

Here is a utility class that I use:

public class AsyncEventListener
{
    private readonly Func<bool> _predicate;

    public AsyncEventListener() : this(() => true)
    {

    }

    public AsyncEventListener(Func<bool> predicate)
    {
        _predicate = predicate;
        Successfully = new Task(() => { });
    }

    public void Listen(object sender, EventArgs eventArgs)
    {
        if (!Successfully.IsCompleted && _predicate.Invoke())
        {
            Successfully.RunSynchronously();
        }
    }

    public Task Successfully { get; }
}

And here is how I use it:

var itChanged = new AsyncEventListener();
someObject.PropertyChanged += itChanged.Listen;

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;

Solution 4

Ideally, you don't. While you certainly can block the async thread, that's a waste of resources, and not ideal.

Consider the canonical example where the user goes to lunch while the button is waiting to be clicked.

If you have halted your asynchronous code while waiting for the input from the user, then it's just wasting resources while that thread is paused.

That said, it's better if in your asynchronous operation, you set the state that you need to maintain to the point where the button is enabled and you're "waiting" on a click. At that point, your GetResults method stops.

Then, when the button is clicked, based on the state that you have stored, you start another asynchronous task to continue the work.

Because the SynchronizationContext will be captured in the event handler that calls GetResults (the compiler will do this as a result of using the await keyword being used, and the fact that SynchronizationContext.Current should be non-null, given you are in a UI application), you can use async/await like so:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();

     // Show dialog/UI element.  This code has been marshaled
     // back to the UI thread because the SynchronizationContext
     // was captured behind the scenes when
     // await was called on the previous line.
     ...

     // Check continue, if true, then continue with another async task.
     if (_continue) await ContinueToGetResultsAsync();
}

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

ContinueToGetResultsAsync is the method that continues to get the results in the event that your button is pushed. If your button is not pushed, then your event handler does nothing.

Solution 5

Simple Helper Class:

public class EventAwaiter<TEventArgs>
{
    private readonly TaskCompletionSource<TEventArgs> _eventArrived = new TaskCompletionSource<TEventArgs>();

    private readonly Action<EventHandler<TEventArgs>> _unsubscribe;

    public EventAwaiter(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe)
    {
        subscribe(Subscription);
        _unsubscribe = unsubscribe;
    }

    public Task<TEventArgs> Task => _eventArrived.Task;

    private EventHandler<TEventArgs> Subscription => (s, e) =>
        {
            _eventArrived.TrySetResult(e);
            _unsubscribe(Subscription);
        };
}

Usage:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;
Share:
92,064

Related videos on Youtube

Max
Author by

Max

Full-stack software developer at Stack Overflow in New York City.

Updated on February 19, 2022

Comments

  • Max
    Max about 2 years

    In my C#/XAML metro app, there's a button which kicks off a long-running process. So, as recommended, I'm using async/await to make sure the UI thread doesn't get blocked:

    private async void Button_Click_1(object sender, RoutedEventArgs e) 
    {
         await GetResults();
    }
    
    private async Task GetResults()
    { 
         // Do lot of complex stuff that takes a long time
         // (e.g. contact some web services)
      ...
    }
    

    Occasionally, the stuff happening within GetResults would require additional user input before it can continue. For simplicity, let's say the user just has to click a "continue" button.

    My question is: how can I suspend the execution of GetResults in such a way that it awaits an event such as the click of another button?

    Here's an ugly way to achieve what I'm looking for: the event handler for the continue" button sets a flag...

    private bool _continue = false;
    private void buttonContinue_Click(object sender, RoutedEventArgs e)
    {
        _continue = true;
    }
    

    ... and GetResults periodically polls it:

     buttonContinue.Visibility = Visibility.Visible;
     while (!_continue) await Task.Delay(100);  // poll _continue every 100ms
     buttonContinue.Visibility = Visibility.Collapsed;
    

    The polling is clearly terrible (busy waiting / waste of cycles) and I'm looking for something event-based.

    Any ideas?

    Btw in this simplified example, one solution would be of course to split up GetResults() into two parts, invoke the first part from the start button and the second part from the continue button. In reality, the stuff happening in GetResults is more complex and different types of user input can be required at different points within the execution. So breaking up the logic into multiple methods would be non-trivial.

  • Daniel Hilgarth
    Daniel Hilgarth over 11 years
    I would have used a ManualResetEvent. Is there an advantage to using SemaphoreSlim or could you use either one?
  • svick
    svick over 11 years
    @DanielHilgarth ManualResetEvent(Slim) doesn't seem to support WaitAsync().
  • Daniel Hilgarth
    Daniel Hilgarth over 11 years
    @svick: Good point. However, as GetResult already is async you could block inside this method without any problems, couldn't you?
  • svick
    svick over 11 years
    @DanielHilgarth No, you couldn't. async doesn't mean “runs on a different thread”, or something like that. It just means “you can use await in this method”. And in this case, blocking inside GetResults() would actually block the UI thread.
  • Daniel Hilgarth
    Daniel Hilgarth over 11 years
    @svick: I agree, async doesn't automatically create a new thread. But in combination with await it does, doesn't it? So in this specific example, it wouldn't block the UI thread, would it?
  • casperOne
    casperOne over 11 years
    @svick "Blocking inside GetResults() would actually block the UI thread." - This is false. Blocking in GetResults would not block the UI thread because until GetResults returns, it's running asynchronously. The background thread/task will block, but not the UI thread. The UI thread was left the moment GetResults was called. Unless the awaitable from GetResult actually doesn't go out to another thread (and it's almost always the case that it does, or it's waiting on some IO completion), it won't block.
  • casperOne
    casperOne over 11 years
    @Gabe await in itself does not guaranted that another thread is created, but it causes everything else after the statement to run as a continuation on the Task or awaitable that you call await on. More often than not, it is some sort of asynchronous operation, which could be IO completion, or something that is on another thread.
  • svick
    svick over 11 years
    @casperOne But there is no background task in the code in the question. GetResults() is called directly from event handler, which means it runs on the UI context. It would not block the UI thread if there was something like Task.Run() or ConfigureAwait(false) somewhere, but there is no such thing in there.
  • svick
    svick over 11 years
    What async thread? There is no code that will not run on the UI thread, both in the original question and in your answer.
  • casperOne
    casperOne over 11 years
    @svick Not true. GetResults returns a Task. await simply says "run the task, and when the task is done, continue the code after this". Given that there is a synchronization context, the call is marshaled back to the UI thread, as it's captured on the await. await is not the same as Task.Wait(), not in the least.
  • casperOne
    casperOne over 11 years
    @svick GetResults returns a Task which is awaited on. This means that the Task runs, and the method that is awaiting actually exits. Then, a continuation is created that continues the remaining code when the thing you await on completes (using a SynchronizationContext, if there is one, and not told to not use it). The Task runs asynchronously, and then the remaining code in the event handler is marshaled back to the UI thread on the synchronization context when the Task is complete.
  • svick
    svick over 11 years
    I didn't say anything about Wait(). But the code in GetResults() will run on the UI thread here, there is no other thread. In other words, yes, await basically does run the task, like you say, but here, that task also runs on the UI thread.
  • svick
    svick over 11 years
    @casperOne Sure, but “runs asynchronously” doesn't mean “runs on another thread”. GetResults() is still on the UI context.
  • casperOne
    casperOne over 11 years
    @svick There's no reason to make the assumption that the task runs on the UI thread, why do you make that assumption? It's possible, but unlikely. And the call is two separate UI calls, technically, one up to the await and then the code after await, there is no blocking. The rest of the code is marshaled back in a continuation and scheduled through the SynchronizationContext.
  • svick
    svick over 11 years
    I'm not making any assumptions, I'm just looking at the code as it is. And as it is, the code inside GetResults() will run on the UI thread. I think it's you that makes assumptions that GetResults() will contain something like Task.Run(). But that's not what I'm talking about, I'm talking about code directly in GetResults().
  • casperOne
    casperOne over 11 years
    @svick No, GetResults continues on the UI context. That's a major difference. Most methods on Task that create Task instances do not capture synchronization context. It's awaiting on them that does. The actual task that does the background work doesn't capture this context at all.
  • svick
    svick over 11 years
    @casperOne My point is that there is no code in this question that explicitly creates a Task, so all of the code will run on the UI thread.
  • casperOne
    casperOne over 11 years
    @svick Based on what exactly? Parts of it will run, but at some point, there has to be something that it awaits on that's going to be Task based, and then that code will run asynchronous, and everything else will continue on the UI thread, but in a continuation call. Those points up to and after the asynchronous task will be on the UI thread, but there will be a background operation (unless you do something like say run synchronously or some other edge case) that is not, and that's where the blocking is broken up.
  • casperOne
    casperOne over 11 years
    @svick You can't have async without await, so what exactly are you awaiting on that isn't run on some other thread, or waiting on IO completion?
  • casperOne
    casperOne over 11 years
    @svick No, but if you have async then you have to have await, unless you're doing something way outside the norm, that await is going to be on a Task that is based on another thread, or waiting on an IO completion which will continue on another thread. Either way, the wait is taken off the UI thread, and then resumed on the UI thread when that particular operation (no matter how many layers deep its buried) is executed. You're not blocking the entire time.
  • casperOne
    casperOne over 11 years
    For others who want to see more, see here: chat.stackoverflow.com/rooms/17937 - @svick and I basically misunderstood each other, but were saying the same thing.
  • casperOne
    casperOne over 11 years
    For others who want to see more, see here: chat.stackoverflow.com/rooms/17937 - @svick and I basically misunderstood each other, but were saying the same thing.
  • James Manning
    James Manning over 11 years
    Since I parse "await an event" as basically the same situation as 'wrap EAP in a task', I'd definitely prefer this approach. IMHO, it's definitely simpler / easier-to-reason-about code.
  • Stephen Cleary
    Stephen Cleary over 11 years
    +1. I had to look this up, so just in case others are interested: SemaphoreSlim.WaitAsync does not just push the Wait onto a thread pool thread. SemaphoreSlim has a proper queue of Tasks that are used to implement WaitAsync.
  • Max
    Max over 11 years
    TaskCompletionSource<T> + await .Task + .SetResult() turns out to be the perfect solution for my scenario - thanks! :-)
  • Alex Essilfie
    Alex Essilfie over 8 years
    Using the TaskCompletionSource<T>, await tcs.Task, tcs.SetResult() turned out to be the easiest way of awaiting an event. Far easier, in fact, than the Event-based Asynchronous Pattern demonstrated on MSDN.
  • Denis P
    Denis P about 6 years
    How would you cleanup the subscription to example.YourEvent?
  • CJBrew
    CJBrew about 6 years
    @DenisP perhaps pass the event into constructor for EventAwaiter?
  • Felix Keil
    Felix Keil about 6 years
    @DenisP I improved the version and run a short test.
  • Denis P
    Denis P about 6 years
    I could see adding IDisposable as well, depending on circumstances. Also, to avoid having to type in the event twice, we could also use Reflection to pass the event name, so then the usage is even simpler. Otherwise, I like the pattern, thank you.
  • Thanasis Ioannidis
    Thanasis Ioannidis over 4 years
    A lot of confusion between parallelism and asnchronism. You can have a perfectly asynchronous environment that runs on one and only thread. Look at javascript on browsers. It runs only on one thread, but the code is mostly asynchronous (with all the callbacks there, and now with Promises). An async environment may be based on an event loop. Everything is processed in one thread but in different moments in time. An async operation is usually sequential in itself, not parallel. It's "do this, when you finish do that, ...etc". Parallelism comes in when you also define how and where to do the job!
  • nawfal
    nawfal over 4 years
    You have completely invented a new event handler mechanism. Maybe this is what delegates in .NET are translated to eventually, but can't expect people to adopt this. Having a return type for the delegate (of the event) itself can put people off to begin with. But good effort, really like how well it is done.
  • cat_in_hat
    cat_in_hat over 4 years
    @nawfal Thanks! I've modified it since to avoid returning a delegate. The source is available here as part of Lara Web Engine, an alternative to Blazor.
  • nawfal
    nawfal over 4 years
    I don't know how this works. How is the Listen method asynchronously executing my custom handler? Wouldn't new Task(() => { }); be instantly completed?
  • tsul
    tsul about 2 years
    The link to the blog gives 403 Forbidden for me, check that please.
  • Drew Noakes
    Drew Noakes about 2 years
    @tsul fixed, thanks.