是否可以等待一个事件,而不是另一种异步方法?(Is it possible to await an

2019-06-18 12:26发布

在我的C#/ XAML Metro应用,有揭开序幕长期运行过程中的按钮。 因此,推荐,我使用异步/等待以确保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内发生的事情将需要更多的用户输入才能继续。 为简单起见,假设用户只是必须点击“继续”按钮。

我的问题是: 我怎么能暂停GetResults的执行,它等待的事件 ,如另一个按钮的点击这样的方式?

下面是一个丑陋的方式来实现我正在寻找:在继续”按钮事件处理程序设置一个标志...

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发生的事情比较复杂,不同类型的用户输入可以在执行中的不同点是必需的。 所以,打破了逻辑分成多个方法将是不平凡的。

Answer 1:

您可以使用的实例SemaphoreSlim类作为一个信号:

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

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

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

可替换地,可以使用的实例TaskCompletionSource <T>类来创建任务<T>表示所述按钮点击的结果:

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

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

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


Answer 2:

当你有你需要的不寻常的事情await上,最简单的答案往往是TaskCompletionSource (或async基于原始-启用TaskCompletionSource )。

在这种情况下,您的需要是很简单的,所以你可以只使用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 ,但你只能“集”的事件一次,事件能有一个“结果”(,我们不使用它在这种情况下,所以我们只是把结果向null ) 。



Answer 3:

下面是我用一个实用工具类:

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;


Answer 4:

理想情况下,你不知道 。 虽然你当然可以阻止异步线程,这是资源的浪费,而且不理想。

考虑用户去吃午饭,而按钮等待被点击的典型例子。

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

这就是说,它的更好,如果你的异步操作,设置,你需要维持该点的按钮被激活,其中国家和你在点击“等待”。 在这一点上,你GetResults方法停止

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

由于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)
  ...
}

ContinueToGetResultsAsync是继续得到您的按钮被按下的事件结果的方法。 如果您的按钮推,那么你的事件处理程序不执行任何操作。



Answer 5:

斯蒂芬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; 
        } 
    } 
}


Answer 6:

简单的辅助类:

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

用法:

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


Answer 7:

随着反应扩展(Rx.Net)

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

var res = await eventObservable.FirstAsync();

你可以用NuGet包System.Reactive添加的Rx

测试的样品:

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


文章来源: Is it possible to await an event instead of another async method?