Is it possible to await an event instead of another async method?
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;
Related videos on Youtube
Max
Full-stack software developer at Stack Overflow in New York City.
Updated on February 19, 2022Comments
-
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 over 11 yearsI would have used a
ManualResetEvent
. Is there an advantage to usingSemaphoreSlim
or could you use either one? -
svick over 11 years@DanielHilgarth
ManualResetEvent(Slim)
doesn't seem to supportWaitAsync()
. -
Daniel Hilgarth over 11 years@svick: Good point. However, as
GetResult
already isasync
you could block inside this method without any problems, couldn't you? -
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 useawait
in this method”. And in this case, blocking insideGetResults()
would actually block the UI thread. -
Daniel Hilgarth over 11 years@svick: I agree,
async
doesn't automatically create a new thread. But in combination withawait
it does, doesn't it? So in this specific example, it wouldn't block the UI thread, would it? -
casperOne over 11 years@svick "Blocking inside
GetResults()
would actually block the UI thread." - This is false. Blocking inGetResults
would not block the UI thread because untilGetResults
returns, it's running asynchronously. The background thread/task will block, but not the UI thread. The UI thread was left the momentGetResults
was called. Unless the awaitable fromGetResult
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 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 theTask
or awaitable that you callawait
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 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 likeTask.Run()
orConfigureAwait(false)
somewhere, but there is no such thing in there. -
svick over 11 yearsWhat async thread? There is no code that will not run on the UI thread, both in the original question and in your answer.
-
casperOne over 11 years@svick Not true.
GetResults
returns aTask
.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 theawait
.await
is not the same asTask.Wait()
, not in the least. -
casperOne over 11 years@svick
GetResults
returns aTask
which is awaited on. This means that theTask
runs, and the method that is awaiting actually exits. Then, a continuation is created that continues the remaining code when the thing youawait
on completes (using aSynchronizationContext
, if there is one, and not told to not use it). TheTask
runs asynchronously, and then the remaining code in the event handler is marshaled back to the UI thread on the synchronization context when theTask
is complete. -
svick over 11 yearsI didn't say anything about
Wait()
. But the code inGetResults()
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 over 11 years@casperOne Sure, but “runs asynchronously” doesn't mean “runs on another thread”.
GetResults()
is still on the UI context. -
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 afterawait
, there is no blocking. The rest of the code is marshaled back in a continuation and scheduled through theSynchronizationContext
. -
svick over 11 yearsI'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 thatGetResults()
will contain something likeTask.Run()
. But that's not what I'm talking about, I'm talking about code directly inGetResults()
. -
casperOne over 11 years@svick No,
GetResults
continues on the UI context. That's a major difference. Most methods onTask
that createTask
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 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 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 over 11 years@svick You can't have
async
withoutawait
, so what exactly are you awaiting on that isn't run on some other thread, or waiting on IO completion? -
casperOne over 11 years@svick No, but if you have
async
then you have to haveawait
, unless you're doing something way outside the norm, thatawait
is going to be on aTask
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 over 11 yearsFor 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 over 11 yearsFor 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 over 11 yearsSince 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 over 11 years+1. I had to look this up, so just in case others are interested:
SemaphoreSlim.WaitAsync
does not just push theWait
onto a thread pool thread.SemaphoreSlim
has a proper queue ofTask
s that are used to implementWaitAsync
. -
Max over 11 yearsTaskCompletionSource<T> + await .Task + .SetResult() turns out to be the perfect solution for my scenario - thanks! :-)
-
Alex Essilfie over 8 yearsUsing 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 about 6 yearsHow would you cleanup the subscription to
example.YourEvent
? -
CJBrew about 6 years@DenisP perhaps pass the event into constructor for EventAwaiter?
-
Felix Keil about 6 years@DenisP I improved the version and run a short test.
-
Denis P about 6 yearsI 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 over 4 yearsA 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 over 4 yearsYou 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 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 over 4 yearsI don't know how this works. How is the Listen method asynchronously executing my custom handler? Wouldn't
new Task(() => { });
be instantly completed? -
tsul about 2 yearsThe link to the blog gives 403 Forbidden for me, check that please.
-
Drew Noakes about 2 years@tsul fixed, thanks.