如何“等待”引发 EventHandler 事件

Posted

技术标签:

【中文标题】如何“等待”引发 EventHandler 事件【英文标题】:How to 'await' raising an EventHandler event 【发布时间】:2012-09-09 05:13:34 【问题描述】:

有时事件模式被用于在 MVVM 应用程序中引发事件,或者子视图模型以像这样松散耦合的方式向其父视图模型发送消息。

父视图模型

searchWidgetViewModel.SearchRequest += (s,e) => 

    SearchOrders(searchWidgitViewModel.SearchCriteria);
;

SearchWidget 视图模型

public event EventHandler SearchRequest;

SearchCommand = new RelayCommand(() => 

    IsSearching = true;
    if (SearchRequest != null) 
    
        SearchRequest(this, EventArgs.Empty);
    
    IsSearching = false;
);

在为 .NET4.5 重构我的应用程序时,我正在制作尽可能多的代码以使用 asyncawait。但是以下不起作用(我真的没想到)

 await SearchRequest(this, EventArgs.Empty);

框架确实这样做是为了调用事件处理程序such as this,但我不确定它是如何做到的?

private async void button1_Click(object sender, RoutedEventArgs e)

   textBlock1.Text = "Click Started";
   await DoWork();
   textBlock2.Text = "Click Finished";

我发现的关于异步引发事件的任何东西isancient,但我在框架中找不到支持这一点的东西。

我如何await 调用一个事件但仍保留在 UI 线程上。

【问题讨论】:

“不起作用”是什么意思? 它无法编译。你不能等待返回 void 的东西 您真的需要等到所有处理程序完成吗?您不能直接启动它们并让它们的async 部分完成而不等待它们吗? 微软现在在Microsoft.VisualStudio.Threading 包中提供了一个AsyncEventHandler 【参考方案1】:

编辑:这不适用于多个订阅者,因此除非您只有一个,否则我不建议您使用它。


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

声明一个代表。这与 EventHandler 相同,但返回一个任务而不是 void

public delegate Task AsyncEventHandler(object sender, EventArgs e);

然后,您可以运行以下命令,只要在父级中声明的处理程序正确使用 asyncawait,这将异步运行:

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

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

【讨论】:

不错的主意,但不幸的是,当任何处理程序完成时,await 也会完成。也就是说,它不会等待所有任务完成。 这是我过去两年一直在使用的 :-) 也许我刚刚侥幸成功,但我必须检查一下,感谢您的评论。 @Laith - 我已经仔细检查过,它似乎对我有用(我在 await 调用之前和之后放置了 debug WriteLine 命令,并且完成的消息直到搜索完成后才出现)。您是否在您的事件处理程序上使用了async(添加到答案中) - 您是否使用了多个订阅者(未经测试) 是的,我说的是多个订阅者。如果您多次使用+=,第一个完成的Task 将结束await 语句。其他任务仍在继续,但没有等待。 [***.com/a/3325424/429091](this answer about events 返回值)表明await 将看到最后一个要执行的处理程序返回的Task。在该答案的 cmets 中进行了讨论,您可以使用 Delegate.GetInvocationList() 单独手动执行处理程序并将其返回包装在 Task.WhenAll() 中。【参考方案2】:

根据 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);

【讨论】:

非常聪明 await SearchRequest?.InvokeAsync(...) -- 不幸的是,这不起作用。我们不能等待 null。 你说得对,当我写这个时,c# 6 还没有发布,我不知道语义是如何工作的:你应该可以写 await (SearchRequest?.InvokeAsync(..) ?? Task.CompletedTask),但不确定是否它实际上更方便。 非常好的解决方案,我会选择强制使用公共构造函数,但以防线程安全问题成为问题 @ed22 "+" 操作符被认为是一个 setter,所以你需要一个公共的 setter 属性然后它才能工作,见例子:dotnetfiddle.net/NdWeLq。如果您不想要公共设置器,您可以用普通方法(即订阅和取消订阅)替换“+”和“-”运算符。【参考方案3】:

正如您所发现的,事件与 asyncawait 并不完美匹配。

UI 处理async 事件的方式与您尝试执行的操作不同。 UI provides a SynchronizationContext to its async events,使他们能够在 UI 线程上恢复。它确实不会“等待”他们。

最佳解决方案 (IMO)

我认为最好的选择是构建您自己的async 友好的发布/订阅系统,使用AsyncCountdownEvent 了解所有处理程序何时完成。

较小的解决方案 #1

async void 方法会在它们开始和结束时通知它们的SynchronizationContext(通过增加/减少异步操作的计数)。所有 UI SynchronizationContexts 都会忽略这些通知,但您可以构建一个包装器来跟踪它并在计数为零时返回。

这是一个示例,使用我的AsyncEx library 中的AsyncContext

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

但是,在本例中,UI 线程Run 中发送消息。

较小的解决方案 #2

您还可以根据嵌套的Dispatcher 框架创建自己的SynchronizationContext,当异步操作的计数达到零时,该框架会自行弹出。但是,您随后会引入重入问题; DoEvents 被故意排除在 WPF 之外。

【讨论】:

【参考方案4】:

回答直接问题:我不认为EventHandler 允许实现与调用者充分通信以允许正确等待。您可能能够使用自定义同步上下文执行技巧,但如果您关心等待处理程序,则处理程序最好能够将其Tasks 返回给调用者。通过将这部分作为委托人的签名,可以更清楚地表明委托人将是awaited。

我建议将Delgate.GetInvocationList() approach described in Ariel’s answer 与tzachs’s answer 的想法混合使用。定义您自己的AsyncEventHandler&lt;TEventArgs&gt; 委托,它返回一个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,因为延续将被编组到调度程序线程。如果,对于您来说,这将神奇地发生例如,您从 winforms 或 WPF 中的 UI 线程调用和 await 事件。否则,您可能必须在突变 EventArgs 时使用锁定,以防您的任何突变发生在线程池上运行的延续中) .

public async Task Run(string[] args)

    if (SomethingHappened != null)
        await SomethingHappened.InvokeAllAsync(this, EventArgs.Empty);

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

请注意,我使用await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty),因为awaitnull 上爆炸。如果需要,您可以使用以下调用模式,但可以说括号很丑,if 样式通常更好,原因有很多:

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

【讨论】:

易于实施,易于使用。太棒了! 非常感谢您提供这个优雅的解决方案。如果有人想查看示例,我将在我的 mongodb 库中使用它here。【参考方案5】:

如果您使用自定义事件处理程序,您可能需要查看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 here 的信息。

【讨论】:

有趣的方法,与其他一些解决方案不同。我个人仍然更喜欢让处理程序返回Task 并通过调用GetInvocationList() 提取它。对于您在此 SO 答案中的使用示例,我认为使用 using (e.GetDeferral()) 模式变体会更好,因为它不那么脆弱。【参考方案6】:

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

查看代码:

var tcs = new TaskCompletionSource<object>();
service.loginCreateCompleted += (object sender, EventArgs e) =>

    tcs.TrySetResult(e.Result);
;
await tcs.Task;

【讨论】:

如果您只需要等待事件的第一个回调,这很好。在等待之后删除事件处理程序可能是个好主意。【参考方案7】:

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

private AsyncEventHandler _asyncEventHandler;
_asyncEventHandler += DoStuffAsync;

Debug.WriteLine("Async invoke incoming!");
await _asyncEventHandler.InvokeAsync(this, EventArgs.Empty);
Debug.WriteLine("Done.");
private async Task DoStuffAsync(object sender, EventArgs args)

    await Task.Delay(1000);
    Debug.WriteLine("hello from async event handler");
    await Task.Delay(1000);

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

【讨论】:

【参考方案8】:

我不清楚您所说的“我如何await 调用事件但仍保留在 UI 线程上”是什么意思。您希望在 UI 线程上执行事件处理程序吗?如果是这种情况,那么您可以执行以下操作:

var h = SomeEvent;
if (h != null)

    await Task.Factory.StartNew(() => h(this, EventArgs.Empty),
        Task.Factory.CancellationToken,
        Task.Factory.CreationOptions,
        TaskScheduler.FromCurrentSynchronizationContext());

它将处理程序的调用包装在Task 对象中,以便您可以使用await,因为您不能将awaitvoid 方法一起使用——这是您的编译错误的根源.

但是,我不确定您希望从中获得什么好处。

我认为那里存在一个基本的设计问题。可以在单击事件上执行一些后台工作,并且您可以实现支持await 的东西。但是,对如何使用 UI 有什么影响?例如如果您有一个 Click 处理程序启动一个需要 2 秒的操作,您是否希望用户能够在操作挂起时单击该按钮?取消和超时是额外的复杂性。我认为这里需要对可用性方面做更多的了解。

【讨论】:

在上面的示例中,我在呼叫周围放置了 'IsSearching=true' 和 'IsSearching=false'。这些是需要在 UI 线程上执行的 MVVM 属性,可能会使“搜索”按钮变灰。最终我的问题是 WPF(调度程序?)可以像上面的“button1_Click”事件一样异步调用 void 事件处理程序,我想知道他们是如何做到的 @Simon_Weaver 好吧,你不能await 一个返回void 的方法,你只能await 一个返回Task&lt;T&gt;Task 的方法。因此,如果要异步调用事件处理程序,则必须将其包装在 Task 对象中,我在答案中已显示。 所以你认为这是 WPF 在调用我声明为异步的事件处理程序时所做的吗? async 修饰符只是告诉编译器生成一个异步状态机来管理它遇到的任何await 关键字 within 方法(更多管理后面的行awaits)。就外部查看该方法而言,它没有任何作用。即,它不会使await 可以使用该方法。 await 仅取决于返回 Task 变体的方法。因此,WPF 可能只是同步调用您的方法。要使用 await 异步调用它,它必须按照我在回答中详细说明的操作:将其包装在 Task 对象中。 您当然可以编写一个返回 Task 的事件处理程序,但没有其他方法可以直接异步调用它。【参考方案9】:

由于委托(并且事件是委托)实现了异步编程模型 (APM),因此您可以使用 TaskFactory.FromAsync 方法。 (另见Tasks and the Asynchronous Programming Model (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;

【讨论】:

【参考方案10】:
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());

【讨论】:

【参考方案11】:

要继续Simon Weaver 的回答,我尝试了以下方法

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

这个接缝可以解决问题。

【讨论】:

【参考方案12】:

这有点来自@Simon_Weaver 的回答,但我觉得它很有用。假设你有一些类RaisesEvents,它有一个事件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
    

【讨论】:

以上是关于如何“等待”引发 EventHandler 事件的主要内容,如果未能解决你的问题,请参考以下文章

如何永远等待直到引发事件? [复制]

如何等待在 UWP 应用中触发 EventHandler?

事件的参数

C# 实例解析事件委托之EventHandler

Net基础恶补

如何删除所有eventhandler