C# 如何“等待”引发 EventHandler 事件
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/12451609/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to 'await' raising an EventHandler event
提问by Simon_Weaver
Sometimes the event pattern is used to raise events in MVVM applications by or a child viewmodel to send a message to its parent viewmodel in a loosely coupled way like this.
有时,事件模式用于在 MVVM 应用程序中引发事件,或者子视图模型以这种松散耦合的方式向其父视图模型发送消息。
Parent ViewModel
父视图模型
searchWidgetViewModel.SearchRequest += (s,e) =>
{
SearchOrders(searchWidgitViewModel.SearchCriteria);
};
SearchWidget ViewModel
SearchWidget 视图模型
public event EventHandler SearchRequest;
SearchCommand = new RelayCommand(() => {
IsSearching = true;
if (SearchRequest != null)
{
SearchRequest(this, EventArgs.Empty);
}
IsSearching = false;
});
In refactoring my application for .NET4.5 I am making as much as code possible to use asyncand await. However the following doesn't work (well I really wasn't expecting it to)
在为 .NET4.5 重构我的应用程序时,我正在制作尽可能多的代码来使用async和await. 但是以下不起作用(好吧我真的没想到它会起作用)
await SearchRequest(this, EventArgs.Empty);
The framework definitely does this to call event handlers such as this, but I'm not sure how it does it?
框架肯定会这样做来调用诸如 this 之类的事件处理程序,但我不确定它是如何做到的?
private async void button1_Click(object sender, RoutedEventArgs e)
{
textBlock1.Text = "Click Started";
await DoWork();
textBlock2.Text = "Click Finished";
}
Anything I've found on the subject of raising events asynchrously isancientbut I can't find something in the framework to support this.
我在异步引发事件的主题上发现的任何东西都是古老的,但我无法在框架中找到支持这一点的东西。
How can I awaitthe calling of an event but remain on the UI thread.
如何await调用事件但保留在 UI 线程上。
采纳答案by Stephen Cleary
Events don't mesh perfectly with asyncand await, as you've discovered.
正如您所发现的,事件与async和并不完全吻合await。
The way UIs handle asyncevents is different than what you're trying to do. The UI provides a SynchronizationContextto its asyncevents, enabling them to resume on the UI thread. It does notever "await" them.
UI 处理async事件的方式与您尝试执行的方式不同。UI为其事件提供了SynchronizationContextasync,使它们能够在 UI 线程上恢复。它不是以往任何时候都“的await”他们。
Best Solution (IMO)
最佳解决方案 (IMO)
I think the best option is to build your own async-friendly pub/sub system, using AsyncCountdownEventto know when all handlers have completed.
我认为最好的选择是构建自己的async友好发布/订阅系统,AsyncCountdownEvent用于了解所有处理程序何时完成。
Lesser Solution #1
较小的解决方案#1
async voidmethods do notify their SynchronizationContextwhen they start and finish (by incrementing/decrementing the count of asynchronous operations). All UI SynchronizationContexts ignore these notifications, but you couldbuild a wrapper that tracks it and returns when the count is zero.
async void方法会SynchronizationContext在开始和结束时通知它们(通过增加/减少异步操作的计数)。所有 UI 都会SynchronizationContext忽略这些通知,但您可以构建一个包装器来跟踪它并在计数为零时返回。
Here's an example, using AsyncContextfrom my AsyncEx library:
这是一个示例,使用AsyncContext来自我的AsyncEx 库:
SearchCommand = new RelayCommand(() => {
IsSearching = true;
if (SearchRequest != null)
{
AsyncContext.Run(() => SearchRequest(this, EventArgs.Empty));
}
IsSearching = false;
});
However, in this example the UI thread is notpumping messages while it's in Run.
然而,在这个例子中,UI线程不同时,它在抽的消息Run。
Lesser Solution #2
较小的解决方案#2
You could also make your own SynchronizationContextbased on a nested Dispatcherframe that pops itself when the count of asynchronous operations reaches zero. However, you then introduce re-entrancy problems; DoEventswas left out of WPF on purpose.
您还可以SynchronizationContext基于嵌套Dispatcher框架制作自己的框架,当异步操作的计数达到零时,该框架会自行弹出。但是,您随后会引入重入问题;DoEvents被故意排除在 WPF 之外。
回答by Simon_Weaver
Edit:This doesn't work well for multiple subscribers, so unless you only have one I wouldn't recommend using this.
编辑:这不适用于多个订阅者,因此除非您只有一个订阅者,否则我不建议使用它。
Feels slightly hacky - but I have never found anything better:
感觉有点hacky - 但我从来没有发现更好的东西:
Declare a delegate. This is identical to EventHandlerbut returns a task instead of void
声明一个代表。这与EventHandler但返回一个任务而不是 void 相同
public delegate Task AsyncEventHandler(object sender, EventArgs e);
You can then run the following and as long as the handler declared in the parent uses asyncand awaitproperly then this will run asynchronously:
然后,您可以运行以下内容,只要在父级中声明的处理程序使用async并await正确,这将异步运行:
if (SearchRequest != null)
{
Debug.WriteLine("Starting...");
await SearchRequest(this, EventArgs.Empty);
Debug.WriteLine("Completed");
}
Sample handler :
样品处理程序:
// declare handler for search request
myViewModel.SearchRequest += async (s, e) =>
{
await SearchOrders();
};
Note: I've never tested this with multiple subscribers and not sure how this will work - so if you need multiple subscribers then make sure to test it carefully.
注意:我从未对多个订阅者进行过测试,也不确定这将如何工作 - 因此,如果您需要多个订阅者,请务必仔细测试。
回答by Peter Ritchie
I'm not clear on what you mean by "How can I awaitthe calling of an event but remain on the UI thread". Do you want the event handler to be executed on the UI thread? If that's the case then you can do something like this:
我不清楚您所说的“如何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());
}
Which wraps the invocation of the handler in a Taskobject so that you can use await, since you can't use awaitwith a voidmethod--which is where your compile error stems from.
它包装了处理程序调用的Task对象,这样就可以使用await,因为你不能用await同一个void方法-这是你的编译错误源于。
But, I'm not sure what benefit you expect to get out of that.
但是,我不确定您希望从中获得什么好处。
I think there's a fundamental design issue there. It's fine to kick of some background work on a click event and you can implement something that supports await. But, what's the effect on how the UI can be used? e.g. if you have a Clickhandler that kicks off an operation that takes 2 seconds, do you want the user to be able to click that button while the operation is pending? Cancellation and timeout are additional complexities. I think much more understanding of the usability aspects needs to be done here.
我认为那里有一个基本的设计问题。可以在单击事件上启动一些后台工作,并且您可以实现支持await. 但是,对如何使用 UI 有什么影响?例如,如果您有一个Click处理程序启动需要 2 秒的操作,您是否希望用户能够在操作挂起时单击该按钮?取消和超时是额外的复杂性。我认为这里需要对可用性方面有更多的了解。
回答by tzachs
Based on Simon_Weaver's answer, I created a helper class that can handle multiple subscribers, and has a similar syntax to c# events.
根据 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);
}
}
}
To use it, you declare it in your class, for example:
要使用它,请在类中声明它,例如:
public AsyncEvent<EventArgs> SearchRequest;
To subscribe an event handler, you'll use the familiar syntax (the same as in Simon_Weaver's answer):
要订阅事件处理程序,您将使用熟悉的语法(与 Simon_Weaver 的回答相同):
myViewModel.SearchRequest += async (s, e) =>
{
await SearchOrders();
};
To invoke the event, use the same pattern we use for c# events (only with InvokeAsync):
要调用事件,请使用我们用于 c# 事件的相同模式(仅适用于 InvokeAsync):
var eventTmp = SearchRequest;
if (eventTmp != null)
{
await eventTmp.InvokeAsync(sender, eventArgs);
}
If using c# 6, one should be able to use the null conditional operator and write this instead:
如果使用 c# 6,则应该能够使用 null 条件运算符并编写以下代码:
await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
回答by Ariel Steiner
To continue on Simon Weaver's answer, I tried the following
为了继续Simon Weaver的回答,我尝试了以下操作
if (SearchRequest != null)
{
foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
{
await onSearchRequest(null, EventArgs.Empty);
}
}
This seams to do the trick.
这接缝可以解决问题。
回答by binki
To answer the direct question: I do not think EventHandlerallows implementations to communicate sufficiently back to the invoker to allow proper awaiting. You might be able to perform tricks with a custom synchronization context, but if you care about waiting for the handlers, it is better that the handlers are able to return their Tasks back to the invoker. By making this part of the delegate's signature, it is clearer that the delegate will be awaited.
回答直接的问题:我认为EventHandler不允许实现与调用者充分沟通以允许适当的等待。您可能能够使用自定义同步上下文执行技巧,但如果您关心等待处理程序,最好处理程序能够将它们的Tasks 返回给调用程序。通过将这部分作为委托人的签名,可以更清楚地表明委托人将被await编辑。
I suggest using the Delgate.GetInvocationList()approach described in Ariel's answermixed with ideas from tzachs's answer. Define your own AsyncEventHandler<TEventArgs>delegate which returns a Task. Then use an extension method to hide the complexity of invoking it correctly. I think this pattern makes sense if you want to execute a bunch of asynchronous event handlers and wait for their results.
我建议使用Delgate.GetInvocationList()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)));
}
This allows you to create a normal .net-style event. Just subscribe to it as you normally would.
这允许您创建一个普通的 .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();
};
}
Then simply remember to use the extension methods to invoke the event rather than invoking them directly. If you want more control in your invocation, you may use the GetHandlers()extension. For the more common case of waiting for all the handlers to complete, just use the convenience wrapper InvokeAllAsync(). In many patterns, events either don't produce anything the caller is interested in or they communicate back to the caller by modifying the passed in EventArgs. (Note, if you can assume a synchronization context with dispatcher-style serialization, your event handlers may mutate the EventArgssafely within their synchronous blocks because the continuations will be marshaled onto the dispatcher thread. This will magically happen for you if, for example, you invoke and awaitthe event from a UI thread in winforms or WPF. Otherwise, you may have to use locking when mutating EventArgsin case if any of your mutations happen in a continuation which gets run on the threadpool).
然后只需记住使用扩展方法来调用事件而不是直接调用它们。如果您想对调用进行更多控制,可以使用GetHandlers()扩展。对于等待所有处理程序完成的更常见情况,只需使用便利包装器InvokeAllAsync()。在许多模式中,事件要么不产生调用者感兴趣的任何内容,要么通过修改传入的EventArgs. (注意,如果你可以假设一个具有调度程序样式序列化的同步上下文,你的事件处理程序可能会EventArgs在它们的同步块中安全地改变 ,因为延续将被编组到调度程序线程上。这会神奇地发生在你身上,例如,你调用和await来自 winforms 或 WPF 中的 UI 线程的事件。否则,您可能必须在变异时使用锁定EventArgs,以防万一您的任何变异发生在线程池上运行的延续中)。
public async Task Run(string[] args)
{
if (SomethingHappened != null)
await SomethingHappened.InvokeAllAsync(this, EventArgs.Empty);
}
This gets you closer to something that looks like a normal event invocation, except that you have to use .InvokeAllAsync(). And, of course, you still have the normal issues that come with events such as needing to guard invocations for events with no subscribers to avoid a NullArgumentException.
这使您更接近于看起来像普通事件调用的东西,只是您必须使用.InvokeAllAsync(). 而且,当然,您仍然会遇到事件带来的正常问题,例如需要保护对没有订阅者的事件的调用以避免NullArgumentException.
Note that I am notusing await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty)because awaitexplodes on null. You could use the following call pattern if you want, but it can be argued that the parens are ugly and the ifstyle is generally better for various reasons:
请注意,我没有使用,await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty)因为await在null. 如果需要,您可以使用以下调用模式,但if由于各种原因,括号很难看,而且样式通常更好:
await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
回答by Scott
Since delegates (and events are delegates) implement the Asynchronous Programming Model (APM), you could use the TaskFactory.FromAsyncmethod. (See also Tasks and the Asynchronous Programming Model (APM).)
由于委托(并且事件是委托)实现了异步编程模型 (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;
}
The above code, however, will invoke the event on a thread pool thread, i.e. it will not capture the current synchronization context. If this is a problem, you could modify it as follows:
然而,上面的代码将在线程池线程上调用事件,即它不会捕获当前的同步上下文。如果这是一个问题,您可以按如下方式修改它:
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;
}
回答by Andrii
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());
}
回答by Pedro Lamas
If you are using custom event handlers, you might want to take a look at the DeferredEvents, as it will allow you to raise and await for the handlers of an event, like this:
如果您使用自定义事件处理程序,您可能需要查看DeferredEvents,因为它允许您引发和等待事件的处理程序,如下所示:
await MyEvent.InvokeAsync(sender, DeferredEventArgs.Empty);
The event handler will do something like this:
事件处理程序将执行以下操作:
public async void OnMyEvent(object sender, DeferredEventArgs e)
{
var deferral = e.GetDeferral();
await DoSomethingAsync();
deferral.Complete();
}
Alternatively, you can use the usingpattern like this:
或者,您可以使用这样的using模式:
public async void OnMyEvent(object sender, DeferredEventArgs e)
{
using (e.GetDeferral())
{
await DoSomethingAsync();
}
}
You can read about the DeferredEvents here.
回答by Keith
This is a little bit derivitive from @Simon_Weaver answer, but I find it useful. presume you have some class RaisesEventswhich has an event RaisesEvents.MyEventand you've got it injected to class MyClass, where you want to subscribe to MyEventProbably better to do the subscribing in an Initialize()method, but for simplicity's sake:
这有点来自@Simon_Weaver 的回答,但我觉得它很有用。假设您有一些RaisesEvents具有事件的类,RaisesEvents.MyEvent并且您已将其注入到 class 中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
}
}

