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

在我的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()拆分为两个部分,从Start按钮调用第一部分,从Continue按钮调用第二部分。实际上,GetResults中发生的事情更加复杂,在执行过程中的不同点可能需要不同类型的用户输入。因此,将逻辑分解为多个方法并不是一件小事。

98192 次浏览

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;

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 __ABC3/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.

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).

Stephen Toub published this AsyncManualResetEvent class 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;
}
}
}

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;

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;

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

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

I'm using my own AsyncEvent class for awaitable events.

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

Here is a small toolbox of six methods, that can be used for converting events to tasks:

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

All these methods are creating a Task that will complete with the next invocation of the associated event. This task can never become faulted or canceled, it may only complete successfully.

Usage example with a standard event (Progress<T>.ProgressChanged):

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

Usage example with a non-standard event:

public static event Action<int> MyEvent;


//...


int result = await EventActionToAsync<int>(h => MyEvent += h, h => MyEvent -= h);

The event is unsubscribed when the task is completed. No mechanism is provided for unsubscribing earlier than that.

Here is a class I used for testing, which support CancellationToken.

This Test method shows us awaiting an instance of ClassWithEvent's MyEvent to be raised. :

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

Here's the event awaiter class.

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

It basically relies on CancellationTokenSource to report back the result. It uses some IL injection to create a delegate to match the event's signature. That delegate is then added as a handler for that event using some reflection. The body of the generate method simply calls another function on the EventAwaiter class, which then reports success using the CancellationTokenSource.

Caution, do not use this, as is, in product. This is meant as a working example.

For instance, IL generation is an expensive process. You should avoid regenerate the same method over and over again, and instead cache these.

AsyncEx has AsyncManualResetEvent for this. You can:

var signal = new AsyncManualResetEvent();
await signal.WaitAsync();

And trigger it with:

signal.Set();