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

时间:2012-10-12 11:53:16

标签: c# microsoft-metro .net-4.5 async-await windows-store-apps

在我的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内发生的事情需要额外的用户输入才能继续。为简单起见,假设用户只需单击“继续”按钮。

我的问题是:如何以等待事件的方式暂停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中发生的事情更复杂,并且在执行中的不同点可能需要不同类型的用户输入。因此,将逻辑分解为多种方法将是非常重要的。

10 个答案:

答案 0 :(得分:197)

您可以使用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;

答案 1 :(得分:65)

当您需要await开启时,最简单的答案通常是TaskCompletionSource(或基于async的某些TaskCompletionSource启用的原语。< / p>

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

答案 2 :(得分:6)

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

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;

答案 3 :(得分:4)

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

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

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

也就是说,如果在异步操作中,将你需要维护的状态设置为启用按钮并且点击时“等待”的状态会更好。此时,您的GetResults方法停止

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

因为SynchronizationContext将在调用GetResults的事件处理程序中捕获(编译器会因使用await关键字而执行此操作,以及{ {3}}应该是非null的,假设你在UI应用程序中),你可以像SynchronizationContext.Current这样使用:

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是在按下按钮时继续获取结果的方法。如果你的按钮不是,那么你的事件处理程序什么都不做。

答案 4 :(得分:3)

Stephen Toub发表了这个AsyncManualResetEvent课程on 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; 
        } 
    } 
}

答案 5 :(得分:3)

简单助手类:

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;

答案 6 :(得分:0)

使用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

经过测试的示例:

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

答案 7 :(得分:0)

我将自己的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;
    }
}

要在引发事件的类中声明一个事件:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

引发事件:

if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());

要订阅活动:

MyControl.Click += async (sender, args) => {
    // await...
}

MyControl.Click += (sender, args) => {
    // synchronous code
    return Task.CompletedTask;
}

答案 8 :(得分:0)

这是一个包含六个方法的小工具箱,可用于将事件转换为任务:

holdingArray

所有这些方法都在创建/// <summary>Converts a .NET event, conforming to the standard .NET event pattern /// based on <see cref="EventHandler"/>, to a Task.</summary> public static Task EventToAsync( Action<EventHandler> addHandler, Action<EventHandler> removeHandler) { var tcs = new TaskCompletionSource<object>(); addHandler(Handler); return tcs.Task; void Handler(object sender, EventArgs e) { removeHandler(Handler); tcs.SetResult(null); } } /// <summary>Converts a .NET event, conforming to the standard .NET event pattern /// based on <see cref="EventHandler{TEventArgs}"/>, to a Task.</summary> public static Task<TEventArgs> EventToAsync<TEventArgs>( Action<EventHandler<TEventArgs>> addHandler, Action<EventHandler<TEventArgs>> removeHandler) { var tcs = new TaskCompletionSource<TEventArgs>(); addHandler(Handler); return tcs.Task; void Handler(object sender, TEventArgs e) { removeHandler(Handler); tcs.SetResult(e); } } /// <summary>Converts a .NET event, conforming to the standard .NET event pattern /// based on a supplied event delegate type, to a Task.</summary> public static Task<TEventArgs> EventToAsync<TDelegate, TEventArgs>( Action<TDelegate> addHandler, Action<TDelegate> removeHandler) { var tcs = new TaskCompletionSource<TEventArgs>(); TDelegate handler = default; Action<object, TEventArgs> genericHandler = (sender, e) => { removeHandler(handler); tcs.SetResult(e); }; handler = (TDelegate)(object)genericHandler.GetType().GetMethod("Invoke") .CreateDelegate(typeof(TDelegate), genericHandler); addHandler(handler); return tcs.Task; } /// <summary>Converts a named .NET event, conforming to the standard .NET event /// pattern based on <see cref="EventHandler"/>, to a Task.</summary> public static Task EventToAsync(object target, string eventName) { var type = target.GetType(); var eventInfo = type.GetEvent(eventName); if (eventInfo == null) throw new InvalidOperationException("Event not found."); var tcs = new TaskCompletionSource<object>(); EventHandler handler = default; handler = new EventHandler((sender, e) => { eventInfo.RemoveEventHandler(target, handler); tcs.SetResult(null); }); eventInfo.AddEventHandler(target, handler); return tcs.Task; } /// <summary>Converts a named .NET event, conforming to the standard .NET event /// pattern based on <see cref="EventHandler{TEventArgs}"/>, to a Task.</summary> public static Task<TEventArgs> EventToAsync<TEventArgs>( object target, string eventName) { var type = target.GetType(); var eventInfo = type.GetEvent(eventName); if (eventInfo == null) throw new InvalidOperationException("Event not found."); var tcs = new TaskCompletionSource<TEventArgs>(); EventHandler<TEventArgs> handler = default; handler = new EventHandler<TEventArgs>((sender, e) => { eventInfo.RemoveEventHandler(target, handler); tcs.SetResult(e); }); eventInfo.AddEventHandler(target, handler); return tcs.Task; } /// <summary>Converts a generic Action-based .NET event to a Task.</summary> public static Task<TArgument> EventActionToAsync<TArgument>( Action<Action<TArgument>> addHandler, Action<Action<TArgument>> removeHandler) { var tcs = new TaskCompletionSource<TArgument>(); addHandler(Handler); return tcs.Task; void Handler(TArgument arg) { removeHandler(Handler); tcs.SetResult(arg); } } ,它将在关联事件的下一次调用时完成。此任务永远不会出错或被取消,只能成功完成。

带有标准事件(Progress<T>.ProgressChanged)的用法示例:

Task

具有非标准事件的用法示例:

var p = new Progress<int>();

//...

int result = await EventToAsync<int>(
    h => p.ProgressChanged += h, h => p.ProgressChanged -= h);

// ...or...

int result = await EventToAsync<EventHandler<int>, int>(
    h => p.ProgressChanged += h, h => p.ProgressChanged -= h);

// ...or...

int result = await EventToAsync<int>(p, "ProgressChanged");

任务完成后,该事件将退订。没有提供取消订阅的机制。

答案 9 :(得分:0)

这是我用来测试的一个类,它支持 CancellationToken。

这个测试方法向我们展示了等待 ClassWithEventMyEvent 的一个实例被引发。 :

    public async Task TestEventAwaiter()
    {
        var cls = new ClassWithEvent();

        Task<bool> isRaisedTask = EventAwaiter<ClassWithEvent>.RunAsync(
            cls, 
            nameof(ClassWithEvent.MyMethodEvent), 
            TimeSpan.FromSeconds(3));

        cls.Raise();
        Assert.IsTrue(await isRaisedTask);
        isRaisedTask = EventAwaiter<ClassWithEvent>.RunAsync(
            cls, 
            nameof(ClassWithEvent.MyMethodEvent), 
            TimeSpan.FromSeconds(1));

        System.Threading.Thread.Sleep(2000);

        Assert.IsFalse(await isRaisedTask);
    }

这是事件等待类。

public class EventAwaiter<TOwner>
{
    private readonly TOwner_owner;
    private readonly string _eventName;
    private readonly TaskCompletionSource<bool> _taskCompletionSource;
    private readonly CancellationTokenSource _elapsedCancellationTokenSource;
    private readonly CancellationTokenSource _linkedCancellationTokenSource;
    private readonly CancellationToken _activeCancellationToken;
    private Delegate _localHookDelegate;
    private EventInfo _eventInfo;

    public static Task<bool> RunAsync(
        TOwner owner,
        string eventName,
        TimeSpan timeout,
        CancellationToken? cancellationToken = null)
    {
        return (new EventAwaiter<TOwner>(owner, eventName, timeout, cancellationToken)).RunAsync(timeout);
    }
    private EventAwaiter(
        TOwner owner,
        string eventName,
        TimeSpan timeout,
        CancellationToken? cancellationToken = null)
    {
        if (owner == null) throw new TypeInitializationException(this.GetType().FullName, new ArgumentNullException(nameof(owner)));
        if (eventName == null) throw new TypeInitializationException(this.GetType().FullName, new ArgumentNullException(nameof(eventName)));

        _owner = owner;
        _eventName = eventName;
        _taskCompletionSource = new TaskCompletionSource<bool>();
        _elapsedCancellationTokenSource = new CancellationTokenSource();
        _linkedCancellationTokenSource =
            cancellationToken == null
                ? null
                : CancellationTokenSource.CreateLinkedTokenSource(_elapsedCancellationTokenSource.Token, cancellationToken.Value);
        _activeCancellationToken = (_linkedCancellationTokenSource ?? _elapsedCancellationTokenSource).Token;

        _eventInfo = typeof(TOwner).GetEvent(_eventName);
        Type eventHandlerType = _eventInfo.EventHandlerType;
        MethodInfo invokeMethodInfo = eventHandlerType.GetMethod("Invoke");
        var parameterTypes = Enumerable.Repeat(this.GetType(),1).Concat(invokeMethodInfo.GetParameters().Select(p => p.ParameterType)).ToArray();
        DynamicMethod eventRedirectorMethod = new DynamicMethod("EventRedirect", typeof(void), parameterTypes);
        ILGenerator generator = eventRedirectorMethod.GetILGenerator();
        generator.Emit(OpCodes.Nop);
        generator.Emit(OpCodes.Ldarg_0);
        generator.EmitCall(OpCodes.Call, this.GetType().GetMethod(nameof(OnEventRaised),BindingFlags.Public | BindingFlags.Instance), null);
        generator.Emit(OpCodes.Ret);
        _localHookDelegate = eventRedirectorMethod.CreateDelegate(eventHandlerType,this);
    }
    private void AddHandler()
    {
        _eventInfo.AddEventHandler(_owner, _localHookDelegate);
    }
    private void RemoveHandler()
    {
        _eventInfo.RemoveEventHandler(_owner, _localHookDelegate);
    }
    private Task<bool> RunAsync(TimeSpan timeout)
    {
        AddHandler();
        Task.Delay(timeout, _activeCancellationToken).
            ContinueWith(TimeOutTaskCompleted);

        return _taskCompletionSource.Task;
    }

    private void TimeOutTaskCompleted(Task tsk)
    {
        RemoveHandler();
        if (_elapsedCancellationTokenSource.IsCancellationRequested) return;

        if (_linkedCancellationTokenSource?.IsCancellationRequested == true)
            SetResult(TaskResult.Cancelled);
        else if (!_taskCompletionSource.Task.IsCompleted)
            SetResult(TaskResult.Failed);

    }

    public void OnEventRaised()
    {
        RemoveHandler();
        if (_taskCompletionSource.Task.IsCompleted)
        {
            if (!_elapsedCancellationTokenSource.IsCancellationRequested)
                _elapsedCancellationTokenSource?.Cancel(false);
        }
        else
        {
            if (!_elapsedCancellationTokenSource.IsCancellationRequested)
                _elapsedCancellationTokenSource?.Cancel(false);
            SetResult(TaskResult.Success);
        }
    }
    enum TaskResult { Failed, Success, Cancelled }
    private void SetResult(TaskResult result)
    {
        if (result == TaskResult.Success)
            _taskCompletionSource.SetResult(true);
        else if (result == TaskResult.Failed)
            _taskCompletionSource.SetResult(false);
        else if (result == TaskResult.Cancelled)
            _taskCompletionSource.SetCanceled();
        Dispose();

    }
    public void Dispose()
    {
        RemoveHandler();
        _elapsedCancellationTokenSource?.Dispose();
        _linkedCancellationTokenSource?.Dispose();
    }
}

它基本上依赖于CancellationTokenSource 来报告结果。 它使用一些 IL 注入来创建一个委托来匹配事件的签名。 然后使用一些反射将该委托添加为该事件的处理程序。 generate 方法的主体只是调用 EventAwaiter 类上的另一个函数,然后使用 CancellationTokenSource 报告成功。

注意,请勿在产品中按原样使用它。这是一个工作示例。

例如,IL 生成是一个昂贵的过程。您应该避免一遍又一遍地重新生成相同的方法,而是缓存它们。