如何'等待'引发EventHandler事件

Sim*_*ver 43 c# events mvvm async-await .net-4.5

有时,事件模式用于通过或者子视图模型在MVVM应用程序中引发事件,以便以松散耦合的方式将消息发送到其父视图模型.

父ViewModel

searchWidgetViewModel.SearchRequest += (s,e) => 
{
    SearchOrders(searchWidgitViewModel.SearchCriteria);
};
Run Code Online (Sandbox Code Playgroud)

SearchWidget ViewModel

public event EventHandler SearchRequest;

SearchCommand = new RelayCommand(() => {

    IsSearching = true;
    if (SearchRequest != null) 
    {
        SearchRequest(this, EventArgs.Empty);
    }
    IsSearching = false;
});
Run Code Online (Sandbox Code Playgroud)

在重构的.NET4.5我的申请,我让尽可能多的代码可以使用asyncawait.但是以下不起作用(我真的没想到)

 await SearchRequest(this, EventArgs.Empty);
Run Code Online (Sandbox Code Playgroud)

该框架确实这样做是为了调用事件处理程序,例如这个,但我不知道它是如何做的呢?

private async void button1_Click(object sender, RoutedEventArgs e)
{
   textBlock1.Text = "Click Started";
   await DoWork();
   textBlock2.Text = "Click Finished";
}
Run Code Online (Sandbox Code Playgroud)

我在异议中提到事件的任何事情都是 古老的,但我无法在框架中找到支持这一点的东西.

如何await调用事件但保留在UI线程上.

Sim*_*ver 31

编辑:这对多个订阅者不起作用,所以除非你只有一个我不建议使用它.


感觉有点hacky - 但我从来没有找到更好的东西:

宣布代表.这与EventHandler但是返回任务而不是void相同

public delegate Task AsyncEventHandler(object sender, EventArgs e);
Run Code Online (Sandbox Code Playgroud)

然后,您可以运行以下操作,只要在父项中声明的处理程序使用asyncawait正确运行,那么它将以异步方式运行:

if (SearchRequest != null) 
{
    Debug.WriteLine("Starting...");
    await SearchRequest(this, EventArgs.Empty);
    Debug.WriteLine("Completed");
}
Run Code Online (Sandbox Code Playgroud)

样品处理器:

 // declare handler for search request
 myViewModel.SearchRequest += async (s, e) =>
 {                    
     await SearchOrders();
 };
Run Code Online (Sandbox Code Playgroud)

注意:我从未使用多个订阅者对此进行测试,也不确定这将如何工作 - 因此,如果您需要多个订阅者,请务必仔细测试.

  • 非常好的主意,但不幸的是,当任何处理程序完成时,`await`就完成了.也就是说,它不会等待所有任务完成. (4认同)
  • [http://stackoverflow.com/a/3325424/429091](这个关于事件返回值的答案)表明`await`会看到最后一个处理程序返回的`Task`来执行.在该答案的评论中讨论,您可以使用[`Delegate.GetInvocationList()`](https://msdn.microsoft.com/en-us/library/system.delegate.getinvocationlist%28v=vs.110%29.aspx )单独手动执行处理程序并将它们的返回包装在`Task.WhenAll()`中. (4认同)
  • @Laith - 我已经仔细检查了它似乎对我有效(我在await调用之前和之后调试了WriteLine命令,并且在搜索完成之后才显示完成的消息).你在事件处理程序上使用`async`(添加回答) - 你是否使用多个订阅者(未经测试) (2认同)
  • 是的,我在谈论多个订阅者.如果多次使用`+ =`,则完成的第一个`Task`结束`await`语句.其他任务仍在继续,但尚未等待. (2认同)
  • 非常适合单个订阅者。谢谢! (2认同)

tza*_*chs 20

根据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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

要使用它,您可以在类中声明它,例如:

public AsyncEvent<EventArgs> SearchRequest;
Run Code Online (Sandbox Code Playgroud)

要订阅事件处理程序,您将使用熟悉的语法(与Simon_Weaver的答案相同):

myViewModel.SearchRequest += async (s, e) =>
{                    
   await SearchOrders();
};
Run Code Online (Sandbox Code Playgroud)

要调用事件,请使用我们用于c#事件的相同模式(仅限InvokeAsync):

var eventTmp = SearchRequest;
if (eventTmp != null)
{
   await eventTmp.InvokeAsync(sender, eventArgs);
}
Run Code Online (Sandbox Code Playgroud)

如果使用c#6,则应该能够使用空条件运算符并写入:

await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
Run Code Online (Sandbox Code Playgroud)

  • 你是对的,当我写这个 c# 6 还没有发布时,我不知道语义是如何工作的:你应该能够编写 `await (SearchRequest?.InvokeAsync(..) ?? Task.CompletedTask )` 不过,不确定它是否真的更方便。 (2认同)
  • @ed22“+”运算符被视为设置器,因此您需要属性上的公共设置器,然后它将起作用,请参阅示例:https://dotnetfiddle.net/NdWeLq。如果您不需要公共设置器,您可以用普通方法(即订阅和取消订阅)替换“+”和“-”运算符。 (2认同)

Ste*_*ary 17

活动不完美网asyncawait,因为你已经发现了.

UI处理async事件的方式与您尝试执行的操作不同.UI 为其事件提供了一个事件SynchronizationContextasync,使它们能够在UI线程上恢复.它不是以往"等待"他们.

最佳解决方案(IMO)

我认为最好的选择是建立你自己async友好的pub/sub系统,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 Code Online (Sandbox Code Playgroud)

但是,在此示例中,UI线程在它进入时不会传送消息Run.

较小的解决方案#2

您也可以SynchronizationContext根据Dispatcher异步操作计数达到零时自动弹出的嵌套框架创建自己的框架.但是,你引入了重新入侵的问题; DoEvents被故意排除在WPF之外.


Sha*_*han 9

您可以使用Microsoft 提供的Microsoft.VisualStudio.ThreadingAsyncEventHandler包中的委托,据我了解,该委托在 Visual Studio 中使用。

private AsyncEventHandler _asyncEventHandler;
Run Code Online (Sandbox Code Playgroud)
_asyncEventHandler += DoStuffAsync;

Debug.WriteLine("Async invoke incoming!");
await _asyncEventHandler.InvokeAsync(this, EventArgs.Empty);
Debug.WriteLine("Done.");
Run Code Online (Sandbox Code Playgroud)
private async Task DoStuffAsync(object sender, EventArgs args)
{
    await Task.Delay(1000);
    Debug.WriteLine("hello from async event handler");
    await Task.Delay(1000);
}
Run Code Online (Sandbox Code Playgroud)

输出:
异步调用传入!
来自异步事件处理程序的问候
完成。


bin*_*nki 5

回答直接的问题:我认为EventHandler不允许实现与调用者充分沟通以允许正确等待.您可以使用自定义同步上下文执行技巧,但如果您关心等待处理程序,则处理程序最好能够将其Task返回给调用者.通过使代表签名的这一部分,代表将被await编辑更清楚.

我建议使用Delgate.GetInvocationList()Ariel的答案中描述方法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)));
}
Run Code Online (Sandbox Code Playgroud)

这允许您创建一个普通的.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();
    };
}
Run Code Online (Sandbox Code Playgroud)

然后只需记住使用扩展方法来调用事件而不是直接调用它们.如果您想在调用中获得更多控制权,可以使用GetHandlers()扩展名.对于等待所有处理程序完成的更常见情况,只需使用便捷包装器即可InvokeAllAsync().在许多模式中,事件要么不产生调用者感兴趣的任何内容,要么通过修改传入来与调用者通信EventArgs.(注意,如果您可以假设具有调度程序样式序列化的同步上下文,则事件处理程序可能会EventArgs在其同步块中安全地进行变更,因为延迟将被编组到调度程序线程上.如果您,例如,您将会神奇地发生这种情况.await从winforms或WPF中的UI线程调用和事件.否则,你可能必须在变异时使用锁定EventArgs,以防你的任何突变发生在一个在线程池上运行的延续中).

public async Task Run(string[] args)
{
    if (SomethingHappened != null)
        await SomethingHappened.InvokeAllAsync(this, EventArgs.Empty);
}
Run Code Online (Sandbox Code Playgroud)

这使您更接近看起来像普通事件调用的东西,除了您必须使用.InvokeAllAsync().而且,当然,您仍然遇到一些常见问题,例如需要保护没有订阅者的事件的调用以避免a NullArgumentException.

请注意,我没有使用await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty)因为await爆炸null.如果你愿意,你可以使用下面的调用模式,但可以认为parens是丑陋的,并且由于if各种原因,样式通常更好:

await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
Run Code Online (Sandbox Code Playgroud)


Mak*_*ich 5

我知道这是一个老问题,但我最好的解决方案是使用TaskCompletionSource

看代码:

var tcs = new TaskCompletionSource<object>();
service.loginCreateCompleted += (object sender, EventArgs e) =>
{
    tcs.TrySetResult(e.Result);
};
await tcs.Task;
Run Code Online (Sandbox Code Playgroud)