Как "ожидать" события EventHandler
Иногда шаблон события используется для повышения событий в приложениях MVVM с помощью модели просмотра или дочерней модели для отправки сообщения в родительскую модель просмотра с помощью слабо связанного способа.
Parent ViewModel
searchWidgetViewModel.SearchRequest += (s,e) =>
{
SearchOrders(searchWidgitViewModel.SearchCriteria);
};
SearchWidget ViewModel
public event EventHandler SearchRequest;
SearchCommand = new RelayCommand(() => {
IsSearching = true;
if (SearchRequest != null)
{
SearchRequest(this, EventArgs.Empty);
}
IsSearching = false;
});
При рефакторинге моего приложения для .NET4.5 я делаю так, как можно использовать код async
и await
. Однако следующее не работает (ну, я действительно этого не ожидал)
await SearchRequest(this, EventArgs.Empty);
Фреймворк определенно делает это для вызова обработчиков событий таких как это, но я не уверен, как он это делает?
private async void button1_Click(object sender, RoutedEventArgs e)
{
textBlock1.Text = "Click Started";
await DoWork();
textBlock2.Text = "Click Finished";
}
Все, что я обнаружил в вопросе поднятия событий асинхронно древний, но я не могу найти что-то в этой области, чтобы поддержать это.
Как я могу await
вызвать событие, но оставаться в потоке пользовательского интерфейса.
Ответы
Ответ 1
События не идеально сочетаются с async
и await
, как вы обнаружили.
То, как пользовательские интерфейсы обрабатывают события async
, отличается от того, что вы пытаетесь сделать. Пользовательский интерфейс предоставляет SynchronizationContext
для его событий async
, позволяя им возобновить работу в потоке пользовательского интерфейса. Он никогда не "ждет" их.
Лучшее решение (IMO)
Я думаю, что лучший вариант - создать собственную async
-дружественную паб/подсистему, используя AsyncCountdownEvent
, чтобы узнать, когда все обработчики завершили.
Малое решение № 1
async void
методы уведомляют их SynchronizationContext
, когда они начинаются и заканчиваются (путем увеличения/уменьшения количества асинхронных операций). Все UI SynchronizationContext
игнорируют эти уведомления, но вы можете создать оболочку, которая отслеживает ее и возвращает, когда счетчик равен нулю.
Вот пример, используя AsyncContext
из моей библиотеки AsyncEx:
SearchCommand = new RelayCommand(() => {
IsSearching = true;
if (SearchRequest != null)
{
AsyncContext.Run(() => SearchRequest(this, EventArgs.Empty));
}
IsSearching = false;
});
Однако в этом примере поток пользовательского интерфейса не передает сообщения, а в Run
.
Малое решение № 2
Вы также можете создать свой собственный SynchronizationContext
на основе вложенного фрейма Dispatcher
, который появляется сам, когда количество асинхронных операций достигает нуля. Однако вы затем вводите проблемы повторного входа; DoEvents
был специально выделен из WPF.
Ответ 2
Изменить: Это не работает для нескольких подписчиков, поэтому, если у вас их нет, я бы не рекомендовал использовать это.
Чувствует себя немного взломанным - но я никогда не находил ничего лучшего:
Объявить делегат. Это идентично EventHandler
, но возвращает задачу вместо void
public delegate Task AsyncEventHandler(object sender, EventArgs e);
Затем вы можете запустить следующее и до тех пор, пока обработчик, объявленный в родительском, правильно использует async
и await
, тогда он будет выполняться асинхронно:
if (SearchRequest != null)
{
Debug.WriteLine("Starting...");
await SearchRequest(this, EventArgs.Empty);
Debug.WriteLine("Completed");
}
Обработчик образца:
// declare handler for search request
myViewModel.SearchRequest += async (s, e) =>
{
await SearchOrders();
};
Примечание. Я никогда не тестировал это с несколькими подписчиками и не уверен, как это будет работать. Поэтому, если вам нужно несколько подписчиков, тогда обязательно проверьте его.
Ответ 3
Основываясь на ответе Simon_Weaver, я создал вспомогательный класс, который может обрабатывать несколько подписчиков, и имеет синтаксис, аналогичный событиям С#.
public class AsyncEvent<TEventArgs> where TEventArgs : EventArgs
{
private readonly List<Func<object, TEventArgs, Task>> invocationList;
private readonly object locker;
private AsyncEvent()
{
invocationList = new List<Func<object, TEventArgs, Task>>();
locker = new object();
}
public static AsyncEvent<TEventArgs> operator +(
AsyncEvent<TEventArgs> e, Func<object, TEventArgs, Task> callback)
{
if (callback == null) throw new NullReferenceException("callback is null");
//Note: Thread safety issue- if two threads register to the same event (on the first time, i.e when it is null)
//they could get a different instance, so whoever was first will be overridden.
//A solution for that would be to switch to a public constructor and use it, but then we'll 'lose' the similar syntax to c# events
if (e == null) e = new AsyncEvent<TEventArgs>();
lock (e.locker)
{
e.invocationList.Add(callback);
}
return e;
}
public static AsyncEvent<TEventArgs> operator -(
AsyncEvent<TEventArgs> e, Func<object, TEventArgs, Task> callback)
{
if (callback == null) throw new NullReferenceException("callback is null");
if (e == null) return null;
lock (e.locker)
{
e.invocationList.Remove(callback);
}
return e;
}
public async Task InvokeAsync(object sender, TEventArgs eventArgs)
{
List<Func<object, TEventArgs, Task>> tmpInvocationList;
lock (locker)
{
tmpInvocationList = new List<Func<object, TEventArgs, Task>>(invocationList);
}
foreach (var callback in tmpInvocationList)
{
//Assuming we want a serial invocation, for a parallel invocation we can use Task.WhenAll instead
await callback(sender, eventArgs);
}
}
}
Чтобы использовать это, вы объявляете это в своем классе, например:
public AsyncEvent<EventArgs> SearchRequest;
Чтобы подписаться на обработчик событий, вы будете использовать знакомый синтаксис (такой же, как в ответе Simon_Weaver):
myViewModel.SearchRequest += async (s, e) =>
{
await SearchOrders();
};
Чтобы вызвать событие, используйте тот же шаблон, который мы используем для событий С# (только с InvokeAsync):
var eventTmp = SearchRequest;
if (eventTmp != null)
{
await eventTmp.InvokeAsync(sender, eventArgs);
}
Если используется С# 6, можно использовать условный оператор null и написать вместо этого:
await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
Ответ 4
Чтобы ответить на прямой вопрос: я не думаю, что EventHandler
позволяет реализациям достаточно общаться с инициатором, чтобы обеспечить надлежащее ожидание. Возможно, вы сможете выполнять трюки с настраиваемым контекстом синхронизации, но если вам нужно дождаться обработчиков, лучше, чтобы обработчики могли вернуть свою Task
обратно вызывающему. Делая эту часть подписи делегатов, становится понятнее, что делегата будут await
.
Я предлагаю использовать Delgate.GetInvocationList()
описанный в ответе Ариельса, в сочетании с идеями из ответа tzachss. Определите свой собственный делегат AsyncEventHandler<TEventArgs>
который возвращает Task
. Затем используйте метод расширения, чтобы скрыть сложность его правильного вызова. Я думаю, что этот шаблон имеет смысл, если вы хотите выполнить кучу асинхронных обработчиков событий и дождаться их результатов.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public delegate Task AsyncEventHandler<TEventArgs>(
object sender,
TEventArgs e)
where TEventArgs : EventArgs;
public static class AsyncEventHandlerExtensions
{
public static IEnumerable<AsyncEventHandler<TEventArgs>> GetHandlers<TEventArgs>(
this AsyncEventHandler<TEventArgs> handler)
where TEventArgs : EventArgs
=> handler.GetInvocationList().Cast<AsyncEventHandler<TEventArgs>>();
public static Task InvokeAllAsync<TEventArgs>(
this AsyncEventHandler<TEventArgs> handler,
object sender,
TEventArgs e)
where TEventArgs : EventArgs
=> Task.WhenAll(
handler.GetHandlers()
.Select(handleAsync => handleAsync(sender, e)));
}
Это позволяет вам создать обычное event
стиле .net. Просто подпишитесь на него, как обычно.
public event AsyncEventHandler<EventArgs> SomethingHappened;
public void SubscribeToMyOwnEventsForNoReason()
{
SomethingHappened += async (sender, e) =>
{
SomethingSynchronous();
// Safe to touch e here.
await SomethingAsynchronousAsync();
// No longer safe to touch e here (please understand
// SynchronizationContext well before trying fancy things).
SomeContinuation();
};
}
Тогда просто не забудьте использовать методы расширения для вызова события, а не вызывать их напрямую. Если вы хотите больше контроля в вашем вызове, вы можете использовать расширение GetHandlers()
. Для более распространенного случая ожидания завершения всех обработчиков просто используйте InvokeAllAsync()
оболочку InvokeAllAsync()
. Во многих шаблонах события либо не производят ничего, что заинтересовало вызывающего, либо сообщаются обратно вызывающему, изменяя передаваемые в EventArgs
. (Обратите внимание, если вы можете предположить контекст синхронизации с сериализацией в стиле диспетчера, ваши обработчики событий могут безопасно изменять EventArgs
в своих синхронных блоках, потому что продолжения будут маршалироваться в потоке диспетчера. Это произойдет волшебным образом, если, например, вы вызываете и await
событий из потока пользовательского интерфейса в WinForms или WPF. в противном случае, возможно, придется использовать блокировку, когда мутирует EventArgs
в случае, если какие - либо из ваших мутаций происходят в продолжении, которое запускаемым на ThreadPool).
public async Task Run(string[] args)
{
if (SomethingHappened != null)
await SomethingHappened.InvokeAllAsync(this, EventArgs.Empty);
}
Это приближает вас к чему-то, что выглядит как обычный вызов события, за исключением того, что вы должны использовать .InvokeAllAsync()
. И, конечно же, у вас все еще есть обычные проблемы, связанные с такими событиями, как необходимость защиты вызовов для событий без подписчиков, чтобы избежать исключения NullArgumentException
.
Обратите внимание, что я не использую await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty)
потому что await
взрывается при null
await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty)
. Вы можете использовать следующий шаблон вызова, если хотите, но можно утверждать, что эти символы ужасны, а стиль if
обычно лучше по разным причинам:
await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
Ответ 5
Я не понимаю, что вы подразумеваете под "Как я могу await
вызвать событие, но оставаться в потоке пользовательского интерфейса". Вы хотите, чтобы обработчик события выполнялся в потоке пользовательского интерфейса? Если это случай, вы можете сделать что-то вроде этого:
var h = SomeEvent;
if (h != null)
{
await Task.Factory.StartNew(() => h(this, EventArgs.Empty),
Task.Factory.CancellationToken,
Task.Factory.CreationOptions,
TaskScheduler.FromCurrentSynchronizationContext());
}
Которая завершает вызов обработчика в объекте Task
, чтобы вы могли использовать await
, так как вы не можете использовать await
с методом void
- вот где ваша ошибка компиляции возникает из.
Но я не уверен, какую выгоду вы ожидаете от этого.
Я думаю, там есть фундаментальная проблема дизайна. Это прекрасно, чтобы ударить некоторые фоновые работы над событием клика, и вы можете реализовать что-то, что поддерживает await
. Но каково влияние на пользовательский интерфейс? например если у вас есть обработчик Click
, который запускает операцию, которая занимает 2 секунды, вы хотите, чтобы пользователь мог нажать эту кнопку во время ожидания операции? Отмена и тайм-аут - дополнительные сложности. Я думаю, что здесь нужно сделать гораздо больше понимания аспектов юзабилити.
Ответ 6
Поскольку делегаты (и события являются делегатами) реализуют модель асинхронного программирования (APM), вы можете использовать метод TaskFactory.FromAsync. (См. Также Задачи и асинхронная модель программирования (APM).)
public event EventHandler SearchRequest;
public async Task SearchCommandAsync()
{
IsSearching = true;
if (SearchRequest != null)
{
await Task.Factory.FromAsync(SearchRequest.BeginInvoke, SearchRequest.EndInvoke, this, EventArgs.Empty, null);
}
IsSearching = false;
}
Приведенный выше код, однако, будет вызывать событие в потоке пула потоков, то есть он не будет захватывать текущий контекст синхронизации. Если это проблема, вы можете изменить ее следующим образом:
public event EventHandler SearchRequest;
private delegate void OnSearchRequestDelegate(SynchronizationContext context);
private void OnSearchRequest(SynchronizationContext context)
{
context.Send(state => SearchRequest(this, EventArgs.Empty), null);
}
public async Task SearchCommandAsync()
{
IsSearching = true;
if (SearchRequest != null)
{
var search = new OnSearchRequestDelegate(OnSearchRequest);
await Task.Factory.FromAsync(search.BeginInvoke, search.EndInvoke, SynchronizationContext.Current, null);
}
IsSearching = false;
}
Ответ 7
public static class FileProcessEventHandlerExtensions
{
public static Task InvokeAsync(this FileProcessEventHandler handler, object sender, FileProcessStatusEventArgs args)
=> Task.WhenAll(handler.GetInvocationList()
.Cast<FileProcessEventHandler>()
.Select(h => h(sender, args))
.ToArray());
}
Ответ 8
Если вы используете пользовательские обработчики событий, вы можете взглянуть на DeferredEvents, так как он позволит вам вызывать и ожидать обработчики события, например:
await MyEvent.InvokeAsync(sender, DeferredEventArgs.Empty);
Обработчик событий будет делать что-то вроде этого:
public async void OnMyEvent(object sender, DeferredEventArgs e)
{
var deferral = e.GetDeferral();
await DoSomethingAsync();
deferral.Complete();
}
Кроме того, вы можете использовать шаблон using
следующим образом:
public async void OnMyEvent(object sender, DeferredEventArgs e)
{
using (e.GetDeferral())
{
await DoSomethingAsync();
}
}
Вы можете прочитать о DeferredEvents здесь.
Ответ 9
Чтобы продолжить на Simon Weaver, я попробовал следующее
if (SearchRequest != null)
{
foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
{
await onSearchRequest(null, EventArgs.Empty);
}
}
Это швы, чтобы сделать трюк.
Ответ 10
Это немного производный от ответа @Simon_Weaver, но я считаю это полезным. Предположим, у вас есть некоторый класс RaisesEvents
который имеет событие RaisesEvents.MyEvent
и вы RaisesEvents.MyEvent
его в класс MyClass
, где вы хотите подписаться на MyEvent
Вероятно, лучше сделать подписку в методе Initialize()
, но для простоты ради:
public class MyClass
{
public MyClass(RaisesEvent otherClass)
{
otherClass.MyEvent += MyAction;
}
private Action MyAction => async () => await ThingThatReturnsATask();
public void Dispose() //it doesn't have to be IDisposable, but you should unsub at some point
{
otherClass.MyEvent -= MyAction;
}
private async Task ThingThatReturnsATask()
{
//async-await stuff in here
}
}