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我的申请,我让尽可能多的代码可以使用async和await.但是以下不起作用(我真的没想到)
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)
然后,您可以运行以下操作,只要在父项中声明的处理程序使用async并await正确运行,那么它将以异步方式运行:
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)
注意:我从未使用多个订阅者对此进行测试,也不确定这将如何工作 - 因此,如果您需要多个订阅者,请务必仔细测试.
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)
Ste*_*ary 17
活动不完美网async和await,因为你已经发现了.
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之外.
您可以使用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)
输出:
异步调用传入!
来自异步事件处理程序的问候
完成。
回答直接的问题:我认为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)
我知道这是一个老问题,但我最好的解决方案是使用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)
| 归档时间: |
|
| 查看次数: |
45456 次 |
| 最近记录: |