首页 文章

是否有可能等待事件而不是另一个异步方法?

提问于
浏览
134

在我的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)
  ...
}

偶尔,GetResults内发生的事情需要额外的用户输入才能继续 . 为简单起见,假设用户只需单击“继续”按钮即可 .

我的问题是: how can I suspend the execution of GetResults in such a way that it awaits an event such as the click of another button?

这是实现我正在寻找的东西的一种丑陋方式:“继续”按钮的事件处理程序设置了一个标志......

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

...和GetResults定期轮询它:

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

民意调查显然非常糟糕(忙碌的等待/浪费周期),我正在寻找基于事件的东西 .

有任何想法吗?

顺便说一下,在这个简化的例子中,一个解决方案当然是将GetResults()分成两部分,从开始按钮调用第一部分,从继续按钮调用第二部分 . 实际上,GetResults中发生的事情更复杂,并且在执行中的不同点可能需要不同类型的用户输入 . 因此,将逻辑分解为多种方法将是非常重要的 .

7 回答

  • 3

    您可以使用SemaphoreSlim Class的实例作为信号:

    private SemaphoreSlim signal = new SemaphoreSlim(0, 1);
    
    // set signal in event
    signal.Release();
    
    // wait for signal somewhere else
    await signal.WaitAsync();
    

    或者,您可以使用TaskCompletionSource<T> Class的实例创建表示按钮单击结果的Task<T>

    private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
    
    // complete task in event
    tcs.SetResult(true);
    
    // wait for task somewhere else
    await tcs.Task;
    
  • 3

    当你有一个不寻常的东西,你需要 await ,最简单的答案通常是 TaskCompletionSource (或一些基于 TaskCompletionSourceasync -enabled原语) .

    在这种情况下,您的需求非常简单,因此您可以直接使用 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);
    }
    

    从逻辑上讲, TaskCompletionSource 就像 async ManualResetEvent ,除了你只能"set"事件一次,事件可以有"result"(在这种情况下,我们没有使用它,所以我们只是将结果设置为 null ) .

  • 0

    这是我使用的实用程序类:

    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; }
    }
    

    以下是我如何使用它:

    var itChanged = new AsyncEventListener();
    someObject.PropertyChanged += itChanged.Listen;
    
    // ... make it change ...
    
    await itChanged.Successfully;
    someObject.PropertyChanged -= itChanged.Listen;
    
  • 4

    理想情况下,你没有 . 虽然你当然可以阻止异步线程,但这是浪费资源,并不理想 .

    考虑用户在按钮等待点击时去吃午餐的规范示例 .

    如果您在等待来自用户的输入时暂停了异步代码,那么它只是在该线程暂停时浪费资源 .

    也就是说,点击一下就会'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'重新"waiting" . 那时,你的 GetResults 方法 stops .

    然后,单击该按钮时,根据您已存储的状态,启动另一个异步任务以继续工作 .

    因为SynchronizationContext将在调用 GetResults 的事件处理程序中捕获(编译器将使用所使用的 await 关键字执行此操作,并且SynchronizationContext.Current应该为非null,因为您在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)
      ...
    }
    

    ContinueToGetResultsAsync 是在按下按钮时继续获得结果的方法 . 如果未按下按钮,则事件处理程序不执行任何操作 .

  • 5

    Stephen Toub发表了这个 AsyncManualResetEventon his blog .

    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; 
            } 
        } 
    }
    
  • 62

    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;
    
  • 187

    With Reactive Extensions (Rx.Net)

    var eventObservable = Observable
                .FromEventPattern<EventArgs>(
                    h => example.YourEvent += h,
                    h => example.YourEvent -= h);
    
    var res = await eventObservable.FirstAsync();
    

    您可以使用Nuget Package 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");
        }
    

相关问题