Как "дождаться" вызова события EventHandler
иногда шаблон события используется для вызова событий в приложениях MVVM дочерней viewmodel или дочерней viewmodel для отправки сообщения родительской viewmodel слабо связанным способом, как это.
Родительский 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";
}
все, что я нашел на тему поднятия событий asynchrously и древние но я не могу найти что-то в рамках, чтобы поддержать это.
как я могу await
вызов события, но остаются в потоке пользовательского интерфейса.
9 ответов
события не идеально сочетаются с async
и await
, Как вы обнаружили.
как UIs обрабатывать async
события отличаются от того, что вы пытаетесь сделать. UI предоставляет SynchronizationContext
в своем async
событий, позволяя им возобновить работу в потоке пользовательского интерфейса. Это не никогда "ждут" их.
лучшее решение (IMO)
я думаю, что лучший вариант-это построить свой собственный async
- дружественный паб / sub система, используя AsyncCountdownEvent
чтобы знать, когда все обработчики завершения.
Меньшее Решение № 1
async void
методы уведомляют их SynchronizationContext
когда они начинаются и заканчиваются (путем увеличения/уменьшения количества асинхронных операций). Все UI SynchronizationContext
s игнорировать эти уведомления, но вы мог бы создайте оболочку, которая отслеживает ее и возвращает, когда счетчик равен нулю.
вот пример, используя AsyncContext
от мой AsyncEx библиотека:
SearchCommand = new RelayCommand(() => {
IsSearching = true;
if (SearchRequest != null)
{
AsyncContext.Run(() => SearchRequest(this, EventArgs.Empty));
}
IsSearching = false;
});
однако, в этом примере поток пользовательского интерфейса не прокачка сообщений, пока он находится в Run
.
Меньшее Решение #2
вы также можете сделать свой собственный SynchronizationContext
на основе вложенных Dispatcher
кадр, который появляется, когда количество асинхронных операций достигает нуля. Однако затем вы вводите проблемы повторного входа;DoEvents
был специально исключен из WPF.
Edit: это не работает хорошо для нескольких подписчиков, поэтому, если у вас нет только одного, я бы не рекомендовал использовать это.
чувствует себя немного хаки-но я никогда не находил ничего лучше:
объявить делегат. Это идентично 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();
};
примечание: Я никогда не тестировал это с несколькими подписчиками и не уверен, как это будет работать - так что если вам нужно несколько подписчиков, то убедитесь, что проверить его тщательно.
основываясь на ответе Simon_Weaver, я создал вспомогательный класс, который может обрабатывать несколько подписчиков и имеет синтаксис, аналогичный событиям c#.
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();
};
чтобы вызвать событие, используйте тот же шаблон, который мы используем для событий c# (только с InvokeAsync):
var eventTmp = SearchRequest;
if (eventTmp != null)
{
await eventTmp.InvokeAsync(sender, eventArgs);
}
если используя c# 6, можно использовать оператор null conditional и написать вместо этого:
await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
ответить на прямой вопрос: я не считаю EventHandler
позволяет реализациям связываться достаточно назад к вызывающему, чтобы позволить надлежащее ожидание. Возможно, вы сможете выполнять трюки с пользовательским контекстом синхронизации, но если вы заботитесь о ожидании обработчиков, лучше, чтобы обработчики могли вернуть их Task
s назад к invoker. Сделав эту часть подписи делегата, становится ясно, что делегат будет await
ed.
I предлагают использовать Delgate.GetInvocationList()
подход, описанный в ответе смешанные с идеями от tzachs это. Определите свой собственный 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)));
}
это позволяет создать обычный .net-стиль event
. Просто подпишитесь на него, как обычно.
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();
};
}
тогда просто не забудьте использовать методы расширения для вызова события, а не вызывать их напрямую. Если вы хотите больше контроля в своем вызове, вы можете использовать
Я не совсем понимаю, что вы подразумеваете под "Как я могу 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
method--вот откуда происходит ошибка компиляции.
но, я не уверен, что выгода, которую вы ожидаете получить от этого.
Я думаю, что там есть фундаментальная проблема дизайна. Это нормально, чтобы пнуть некоторую фоновую работу над событием click, и вы можете реализовать то, что поддерживает await
. Но каково влияние на то, как можно использовать пользовательский интерфейс? например, если у вас есть Click
обработчик, который запускает операцию, которая занимает 2 секунды, вы хотите, чтобы пользователь мог нажать эту кнопку, пока операция находится в ожидании? Отмена и ожидания дополнительные сложности. Я думаю, что здесь нужно сделать гораздо больше понимания аспектов удобства использования.
поскольку делегаты (и события являются делегатами) реализуют модель асинхронного программирования (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;
}
продолжить Саймон Уиверответ, я попробовал следующее
if (SearchRequest != null)
{
foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
{
await onSearchRequest(null, EventArgs.Empty);
}
}
Это швы, чтобы сделать трюк.
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());
}
если вы используете пользовательские обработчики событий, вы можете захотеть взглянуть на 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 здесь.