C# 是否可以等待事件而不是另一种异步方法?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/12858501/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Is it possible to await an event instead of another async method?
提问by Max
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:
在我的 C#/XAML Metro 应用程序中,有一个按钮可以启动一个长期运行的进程。因此,按照建议,我使用 async/await 来确保 UI 线程不会被阻塞:
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.
有时,GetResults 中发生的事情需要额外的用户输入才能继续。为简单起见,假设用户只需单击“继续”按钮。
My question is: how can I suspend the execution of GetResults in such a way that it awaits an eventsuch as the click of another button?
我的问题是:如何以等待事件(例如单击另一个按钮)的方式暂停 GetResults 的执行?
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:
...和 GetResults 定期轮询它:
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.
顺便说一句,在这个简化的示例中,一个解决方案当然是将 GetResults() 分成两部分,从开始按钮调用第一部分,从继续按钮调用第二部分。实际上,GetResults 中发生的事情更为复杂,在执行过程中的不同点可能需要不同类型的用户输入。因此,将逻辑分解为多个方法将是非常重要的。
采纳答案by dtb
You can use an instance of the SemaphoreSlim Classas a signal:
您可以使用SemaphoreSlim 类的实例作为信号:
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> Classto create a Task<T>that represents the result of the button click:
或者,您可以使用TaskCompletionSource<T> 类的实例来创建一个Task<T>来表示按钮单击的结果:
private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
// complete task in event
tcs.SetResult(true);
// wait for task somewhere else
await tcs.Task;
回答by casperOne
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 GetResultsmethod stops.
也就是说,最好在异步操作中,将需要维护的状态设置为启用按钮并“等待”单击的点。此时,您的GetResults方法将停止。
Then, when the button isclicked, based on the state that you have stored, you start another asynchronous taskto continue the work.
然后,当按钮被点击后,根据您所储存的状态,启动另一个异步任务,继续工作。
Because the SynchronizationContextwill be captured in the event handler that calls GetResults(the compiler will do this as a result of using the awaitkeyword being used, and the fact that SynchronizationContext.Currentshould be non-null, given you are in a UI application), you can use async/awaitlike so:
因为SynchronizationContext将在调用的事件处理程序中被捕获GetResults(编译器会因为使用所使用的await关键字而执行此操作,并且SynchronizationContext.Current应该是非空的,因为您在 UI 应用程序中),您可以使用async/await像这样:
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)
...
}
ContinueToGetResultsAsyncis the method that continues to get the results in the event that your button is pushed. If your button is notpushed, then your event handler does nothing.
ContinueToGetResultsAsync是在您的按钮被按下时继续获得结果的方法。如果您的按钮没有被按下,那么您的事件处理程序什么都不做。
回答by Stephen Cleary
When you have an unusual thing you need to awaiton, the easiest answer is often TaskCompletionSource(or some async-enabled primitive based on TaskCompletionSource).
当您需要处理不寻常的事情await时,最简单的答案通常是TaskCompletionSource(或async基于 的某些启用的原语TaskCompletionSource)。
In this case, your need is quite simple, so you can just use TaskCompletionSourcedirectly:
在这种情况下,您的需求非常简单,因此您可以直接使用TaskCompletionSource:
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, TaskCompletionSourceis like an asyncManualResetEvent, 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).
从逻辑上讲,TaskCompletionSource就像asyncManualResetEvent,除了您只能“设置”一次事件并且事件可以有“结果”(在这种情况下,我们没有使用它,所以我们只是将结果设置为null)。
回答by Drew Noakes
Stephen Toub published this AsyncManualResetEventclass on his blog.
Stephen Toub在他的博客上发布了这AsyncManualResetEvent门课。
public class AsyncManualResetEvent
{
private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();
public Task WaitAsync() { return m_tcs.Task; }
public void Set()
{
var tcs = m_tcs;
Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default);
tcs.Task.Wait();
}
public void Reset()
{
while (true)
{
var tcs = m_tcs;
if (!tcs.Task.IsCompleted ||
Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs)
return;
}
}
}
回答by Anders Skovborg
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;
回答by Felix Keil
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;
回答by Felix Keil
With Reactive Extensions (Rx.Net)
var eventObservable = Observable
.FromEventPattern<EventArgs>(
h => example.YourEvent += h,
h => example.YourEvent -= h);
var res = await eventObservable.FirstAsync();
You can add Rx with Nuget Package System.Reactive
您可以使用 Nuget 包 System.Reactive 添加 Rx
Tested Sample:
测试样品:
private static event EventHandler<EventArgs> _testEvent;
private static async Task Main()
{
var eventObservable = Observable
.FromEventPattern<EventArgs>(
h => _testEvent += h,
h => _testEvent -= h);
Task.Delay(5000).ContinueWith(_ => _testEvent?.Invoke(null, new EventArgs()));
var res = await eventObservable.FirstAsync();
Console.WriteLine("Event got fired");
}
回答by cat_in_hat
I'm using my own AsyncEvent class for awaitable events.
我正在使用我自己的 AsyncEvent 类来处理可等待的事件。
public delegate Task AsyncEventHandler<T>(object sender, T args) where T : EventArgs;
public class AsyncEvent : AsyncEvent<EventArgs>
{
public AsyncEvent() : base()
{
}
}
public class AsyncEvent<T> where T : EventArgs
{
private readonly HashSet<AsyncEventHandler<T>> _handlers;
public AsyncEvent()
{
_handlers = new HashSet<AsyncEventHandler<T>>();
}
public void Add(AsyncEventHandler<T> handler)
{
_handlers.Add(handler);
}
public void Remove(AsyncEventHandler<T> handler)
{
_handlers.Remove(handler);
}
public async Task InvokeAsync(object sender, T args)
{
foreach (var handler in _handlers)
{
await handler(sender, args);
}
}
public static AsyncEvent<T> operator+(AsyncEvent<T> left, AsyncEventHandler<T> right)
{
var result = left ?? new AsyncEvent<T>();
result.Add(right);
return result;
}
public static AsyncEvent<T> operator-(AsyncEvent<T> left, AsyncEventHandler<T> right)
{
left.Remove(right);
return left;
}
}
To declare an event in the class that raises events:
要在引发事件的类中声明事件:
public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;
To raise the events:
要引发事件:
if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());
To subscribe to the events:
要订阅事件:
MyControl.Click += async (sender, args) => {
// await...
}
MyControl.Click += (sender, args) => {
// synchronous code
return Task.CompletedTask;
}

