it-roy-ru.com

Как "подождать" вызова события EventHandler

Иногда шаблон событий используется для создания событий в приложениях MVVM или дочерней viewmodel для отправки сообщения в родительскую viewmodel в слабосвязанной форме, как это. 

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 вызывать событие, но оставаться в потоке пользовательского интерфейса.

37
Simon_Weaver

Как вы обнаружили, события не идеально сочетаются с async и await.

То, как пользовательские интерфейсы обрабатывают события async, отличается от того, что вы пытаетесь сделать. Пользовательский интерфейс предоставляет SynchronizationContext для своих событий async , позволяя им возобновить работу в потоке пользовательского интерфейса. Это не когда-либо "ждет" их.

Лучшее решение (ИМО)

Я думаю, что лучший вариант - это создать свою собственную async-friendly паб/подсистему, используя AsyncCountdownEvent , чтобы узнать, когда все обработчики завершены.

Малое решение № 1

Методы async void уведомляют свои SynchronizationContext о начале и завершении (путем увеличения/уменьшения числа асинхронных операций). Все пользовательские интерфейсы SynchronizationContext игнорируют эти уведомления, но вы могли создали оболочку, которая отслеживает ее и возвращает, когда счетчик равен нулю.

Вот пример использования AsyncContext из моей библиотеки AsyncEx :

SearchCommand = new RelayCommand(() => {
  IsSearching = true;
  if (SearchRequest != null) 
  {
    AsyncContext.Run(() => SearchRequest(this, EventArgs.Empty));
  }
  IsSearching = false;
});

Однако в этом примере поток пользовательского интерфейса not перекачивает сообщения, пока он находится в Run.

Малое решение № 2

Вы также можете создать свой собственный SynchronizationContext на основе вложенного фрейма Dispatcher, который появляется, когда количество асинхронных операций достигает нуля. Однако затем вы вводите проблемы повторного входа; DoEvents был специально исключен из WPF.

16
Stephen Cleary

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

Примечание: я никогда не проверял это с несколькими подписчиками и не уверен, как это будет работать - поэтому, если вам нужно несколько подписчиков, обязательно проверьте это внимательно.

27
Simon_Weaver

Основываясь на ответе 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 и написать вместо этого:

await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
19
tzachs

Чтобы ответить на прямой вопрос: я не думаю, что EventHandler позволяет реализациям в достаточной степени сообщать инициатору, чтобы обеспечить надлежащее ожидание. Возможно, вы сможете выполнять трюки с настраиваемым контекстом синхронизации, но если вам нужно дождаться обработчиков, лучше, чтобы обработчики могли возвращать свои Tasks обратно вызывающему. Делая эту часть подписи делегата, становится яснее, что делегат будет awaited.

Я предлагаю использовать Delgate.GetInvocationList() подход, описанный в ответе Ариэля смешанный с идеями из ответа Цахса . Определите свой собственный 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();
    };
}

Тогда просто не забудьте использовать методы расширения для вызова события, а не вызывать их напрямую. Если вы хотите больше контроля в вашем вызове, вы можете использовать расширение GetHandlers(). Для более распространенного случая ожидания завершения всех обработчиков просто используйте вспомогательную оболочку InvokeAllAsync(). Во многих шаблонах события либо не производят ничего интересного для вызывающего, либо они сообщают вызывающему, изменяя переданный в EventArgs. (Обратите внимание, если вы можете предположить контекст синхронизации с сериализацией в стиле диспетчера, ваши обработчики событий могут безопасно изменять EventArgs в своих синхронных блоках, потому что продолжения будут маршалироваться в поток диспетчера. Это будет происходить волшебным образом, если, например, Вы вызываете и await событие из потока пользовательского интерфейса в winforms или WPF. В противном случае вам, возможно, придется использовать блокировку при мутировании EventArgs в случае, если какая-либо из ваших мутаций произойдет в продолжении, которое запускается в пуле потоков).

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. Если хотите, вы можете использовать следующий шаблон вызова, но можно утверждать, что эти символы ужасны, а стиль if обычно лучше по разным причинам:

await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
5
binki

Поскольку делегаты (а события являются делегатами) реализуют модель асинхронного программирования (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;
}
2
Scott

Я не совсем понимаю, что вы подразумеваете под «Как я могу 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 секунды, хотите ли вы, чтобы пользователь мог нажимать эту кнопку, пока операция находится в состоянии ожидания? Отмена и время ожидания являются дополнительными сложностями. Я думаю, что здесь должно быть гораздо больше понимания аспектов юзабилити.

2
Peter Ritchie

Чтобы продолжить ответ Саймона Уивера , я попробовал следующее

        if (SearchRequest != null)
        {
            foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
            {
                await onSearchRequest(null, EventArgs.Empty);
            }
        }

Это швы, чтобы сделать трюк.

0
Ariel Steiner
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());
}
0
Andrii

Если вы используете пользовательские обработчики событий, вы можете взглянуть на 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 здесь .

0
Pedro Lamas