有时,事件模式用于通过子视图模型在MVVM应用程序中引发事件,以便以松散耦合的方式向其父视图模型发送消息。
父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);
框架肯定会调用事件处理程序such as this,但我不确定它是如何做到的?
private async void button1_Click(object sender, RoutedEventArgs e)
{
textBlock1.Text = "Click Started";
await DoWork();
textBlock2.Text = "Click Finished";
}
我发现任何关于异议提出事件的问题is ancient但是我在框架中找不到支持这一点的东西。
我如何await
调用事件但保留在UI线程上。
答案 0 :(得分:28)
修改:这对多个订阅者不起作用,所以除非你只有一个我不建议使用它。
感觉有点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();
};
注意:我从未使用多个订阅者对此进行测试,也不确定这是如何工作的 - 所以如果您需要多个订阅者,请务必仔细测试。
答案 1 :(得分: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);
}
}
}
要使用它,您可以在课程中声明它,例如:
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,则应该能够使用空条件运算符并改为编写:
await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
答案 2 :(得分:17)
正如您所发现的,事件与async
和await
不完全吻合。
UI处理async
事件的方式与您尝试的方式不同。 UI provides a SynchronizationContext
to its async
events,使他们能够在UI线程上恢复。它不永远“等待”他们。
最佳解决方案(IMO)
我认为最好的选择是建立自己的async
友好的发布/订阅系统,使用AsyncCountdownEvent
来了解所有处理程序何时完成。
较小的解决方案#1
async void
方法在它们开始和结束时(通过递增/递减异步操作的计数)通知它们的SynchronizationContext
。所有用户界面SynchronizationContext
都会忽略这些通知,但您可以构建一个跟踪它的包装器,并在计数为零时返回。
以下是一个示例,使用AsyncEx library中的AsyncContext
:
SearchCommand = new RelayCommand(() => {
IsSearching = true;
if (SearchRequest != null)
{
AsyncContext.Run(() => SearchRequest(this, EventArgs.Empty));
}
IsSearching = false;
});
但是,在这个示例中,UI线程不在Run
中传送消息。
较小的解决方案#2
您还可以根据嵌套的SynchronizationContext
框架创建自己的Dispatcher
,当异步操作的计数达到零时,该框架会自动弹出。但是,你引入了重新入侵的问题; DoEvents
故意被排除在WPF之外。
答案 3 :(得分:5)
回答直接问题:我认为EventHandler
不允许实现与调用者充分沟通以允许正确等待。您可以使用自定义同步上下文执行技巧,但如果您关心等待处理程序,则处理程序最好能够将Task
返回给调用者。通过使代表签名的这一部分,代表将await
更清楚。
我建议使用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
上爆炸。如果你愿意,你可以使用下面的调用模式,但可以认为parens是丑陋的,并且if
样式通常由于各种原因更好:
await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
答案 4 :(得分:3)
我知道这是一个老问题,但我最好的解决方案是使用 TaskCompletionSource。
查看代码:
var tcs = new TaskCompletionSource<object>();
service.loginCreateCompleted += (object sender, EventArgs e) =>
{
tcs.TrySetResult(e.Result);
};
await tcs.Task;
答案 5 :(得分:2)
我不清楚你的意思是“我怎样才能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秒的操作,你是否希望用户能够在操作挂起时单击该按钮?取消和超时是额外的复杂性。我认为需要在这里更多地了解可用性方面。
答案 6 :(得分:2)
由于委托(和事件是委托)实现异步编程模型(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;
}
答案 7 :(得分:1)
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 :(得分:1)
如果您使用的是自定义事件处理程序,则可能要看一下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。
答案 9 :(得分:0)
要继续Simon Weaver的答案,我尝试了以下
if (SearchRequest != null)
{
foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
{
await onSearchRequest(null, EventArgs.Empty);
}
}
这样做就可以了。
答案 10 :(得分:0)
这是@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
}
}