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

вот пример, используя 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 позволяет реализациям связываться достаточно назад к вызывающему, чтобы позволить надлежащее ожидание. Возможно, вы сможете выполнять трюки с пользовательским контекстом синхронизации, но если вы заботитесь о ожидании обработчиков, лучше, чтобы обработчики могли вернуть их Tasks назад к invoker. Сделав эту часть подписи делегата, становится ясно, что делегат будет awaited.

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 здесь.