Можно ли ожидать событие вместо другого асинхронного метода?
В моем приложении C#/XAML metro есть кнопка, которая запускает длительный процесс. Итак, как я рекомендовал, я использую async/await, чтобы убедиться, что поток пользовательского интерфейса не заблокирован:
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, являются более сложными, и в разных точках выполнения могут потребоваться разные типы пользовательского ввода. Так что разбить логику на несколько методов было бы нетривиально.
11 ответов
Вы можете использовать экземпляр класса SemaphoreSlim в качестве сигнала:
private SemaphoreSlim signal = new SemaphoreSlim(0, 1);
// set signal in event
signal.Release();
// wait for signal somewhere else
await signal.WaitAsync();
Кроме того, вы можете использовать экземпляр класса TaskCompletionSource
private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
// complete task in event
tcs.SetResult(true);
// wait for task somewhere else
await tcs.Task;
Когда у вас есть необычная вещь, вам нужно 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
).
Вот полезный класс, который я использую:
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;
Простой Помощник Класс:
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;
В идеале вы этого не делаете. Хотя вы, конечно, можете заблокировать асинхронный поток, это пустая трата ресурсов, а не идеальная.
Рассмотрим канонический пример, когда пользователь идет на обед, пока кнопка ожидает нажатия.
Если вы остановили свой асинхронный код во время ожидания ввода от пользователя, то это просто напрасная трата ресурсов, пока этот поток приостановлен.
Тем не менее, лучше, если в вашей асинхронной операции вы устанавливаете состояние, которое необходимо поддерживать, до точки, в которой кнопка включена, и вы "ждете" щелчка. В этот момент ваш GetResults
метод останавливается.
Затем, когда кнопка нажата, в зависимости от состояния, которое вы сохранили, вы запускаете другую асинхронную задачу, чтобы продолжить работу.
Поскольку SynchronizationContext
будет захвачен в обработчике событий, который вызывает GetResults
(компилятор сделает это в результате использования await
ключевое слово и тот факт, что SynchronizationContext.Current должен быть ненулевым, если вы находитесь в приложении с пользовательским интерфейсом), вы можете использовать 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
это метод, который продолжает получать результаты в случае нажатия вашей кнопки. Если ваша кнопка не нажата, то ваш обработчик событий ничего не делает.
Я использую свой собственный класс 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;
}
Стивен Туб опубликовал это 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;
}
}
}
С реактивными расширениями (Rx.Net)
var eventObservable = Observable
.FromEventPattern<EventArgs>(
h => example.YourEvent += h,
h => example.YourEvent -= h);
var res = await eventObservable.FirstAsync();
Вы можете добавить Rx с помощью Nuget Package System.Reactive
Протестированный образец:
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");
}
Вот небольшой набор из шести методов, которые можно использовать для преобразования событий в задачи:
/// <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);
}
}
Все эти методы создают
Task
это завершится при следующем вызове связанного события. Эта задача никогда не может быть сброшена или отменена, она может быть выполнена только успешно.
Пример использования со стандартным событием ( <tcode id="51052"></tcode>):
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");
Пример использования с нестандартным событием:
public static event Action<int> MyEvent;
//...
int result = await EventActionToAsync<int>(h => MyEvent += h, h => MyEvent -= h);
Событие отменяется по завершении задачи. Механизма отказа от подписки раньше этого не предусмотрено.
Вот класс, который я использовал для тестирования, который поддерживает CancellationToken.
Этот метод испытания показывают нам , ждущий экземпляр ClassWithEvent «s MyEvent быть поднято. :
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 - дорогостоящий процесс. Вам следует избегать повторного создания одного и того же метода снова и снова, а вместо этого кэшировать их.
Для этого у AsyncEx есть AsyncManualResetEvent . Ты можешь:
var signal = new AsyncManualResetEvent();
await signal.WaitAsync();
И активируйте его с помощью:
signal.Set();