Подтвердить что ты не робот

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

4b9b3361

Ответ 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
    }
}