如何“等待”引发 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 重构我的应用程序时,我正在制作尽可能多的代码以使用 async
和 await
。但是以下不起作用(我真的没想到)
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);
然后,您可以运行以下命令,只要在父级中声明的处理程序正确使用 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();
;
注意:我从未对多个订阅者进行过测试,也不确定这将如何工作 - 所以如果您需要多个订阅者,请务必仔细测试。
【讨论】:
不错的主意,但不幸的是,当任何处理程序完成时,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】:
正如您所发现的,事件与 async
和 await
并不完美匹配。
UI 处理async
事件的方式与您尝试执行的操作不同。 UI provides a SynchronizationContext
to its async
events,使他们能够在 UI 线程上恢复。它确实不会“等待”他们。
最佳解决方案 (IMO)
我认为最好的选择是构建您自己的async
友好的发布/订阅系统,使用AsyncCountdownEvent
了解所有处理程序何时完成。
较小的解决方案 #1
async void
方法会在它们开始和结束时通知它们的SynchronizationContext
(通过增加/减少异步操作的计数)。所有 UI SynchronizationContext
s 都会忽略这些通知,但您可以构建一个包装器来跟踪它并在计数为零时返回。
这是一个示例,使用我的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
允许实现与调用者充分通信以允许正确等待。您可能能够使用自定义同步上下文执行技巧,但如果您关心等待处理程序,则处理程序最好能够将其Task
s 返回给调用者。通过将这部分作为委托人的签名,可以更清楚地表明委托人将是await
ed。
我建议将Delgate.GetInvocationList()
approach described in Ariel’s answer 与tzachs’s answer 的想法混合使用。定义您自己的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
,因为延续将被编组到调度程序线程。如果,对于您来说,这将神奇地发生例如,您从 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)
,因为await
在null
上爆炸。如果需要,您可以使用以下调用模式,但可以说括号很丑,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
,因为您不能将await
与void
方法一起使用——这是您的编译错误的根源.
但是,我不确定您希望从中获得什么好处。
我认为那里存在一个基本的设计问题。可以在单击事件上执行一些后台工作,并且您可以实现支持await
的东西。但是,对如何使用 UI 有什么影响?例如如果您有一个 Click
处理程序启动一个需要 2 秒的操作,您是否希望用户能够在操作挂起时单击该按钮?取消和超时是额外的复杂性。我认为这里需要对可用性方面做更多的了解。
【讨论】:
在上面的示例中,我在呼叫周围放置了 'IsSearching=true' 和 'IsSearching=false'。这些是需要在 UI 线程上执行的 MVVM 属性,可能会使“搜索”按钮变灰。最终我的问题是 WPF(调度程序?)可以像上面的“button1_Click”事件一样异步调用 void 事件处理程序,我想知道他们是如何做到的 @Simon_Weaver 好吧,你不能await
一个返回void
的方法,你只能await
一个返回Task<T>
或Task
的方法。因此,如果要异步调用事件处理程序,则必须将其包装在 Task
对象中,我在答案中已显示。
所以你认为这是 WPF 在调用我声明为异步的事件处理程序时所做的吗?
async
修饰符只是告诉编译器生成一个异步状态机来管理它遇到的任何await
关键字 within 方法(更多管理后面的行await
s)。就外部查看该方法而言,它没有任何作用。即,它不会使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 事件的主要内容,如果未能解决你的问题,请参考以下文章