我如何在 C# 中等待事件?
How do I await events in C#?
我正在创建一个包含一系列事件的 class,其中之一是 GameShuttingDown
。触发此事件时,我需要调用事件处理程序。此事件的目的是通知用户游戏正在关闭,他们需要保存数据。保存是可等待的,而事件不是。因此,当处理程序被调用时,游戏会在等待的处理程序完成之前关闭。
public event EventHandler<EventArgs> GameShuttingDown;
public virtual async Task ShutdownGame()
{
await this.NotifyGameShuttingDown();
await this.SaveWorlds();
this.NotifyGameShutDown();
}
private async Task SaveWorlds()
{
foreach (DefaultWorld world in this.Worlds)
{
await this.worldService.SaveWorld(world);
}
}
protected virtual void NotifyGameShuttingDown()
{
var handler = this.GameShuttingDown;
if (handler == null)
{
return;
}
handler(this, new EventArgs());
}
活动报名
// The game gets shut down before this completes because of the nature of how events work
DefaultGame.GameShuttingDown += async (sender, args) => await this.repo.Save(blah);
我知道事件的签名是 void EventName
所以让它异步基本上是即发即忘。我的引擎大量使用事件来通知 3rd 方开发人员(和多个内部组件)引擎内正在发生事件并让他们对它们做出反应。
是否有好的途径可以用我可以使用的基于异步的东西替换事件?我不确定我是否应该将 BeginShutdownGame
和 EndShutdownGame
与回调一起使用,但这很痛苦,因为只有调用源可以传递回调,而不是任何插入到引擎,这是我从事件中得到的。如果服务器调用 game.ShutdownGame()
,引擎插件和/或引擎中的其他组件无法传递它们的回调,除非我连接某种注册方法,保留回调集合。
任何关于 preferred/recommended 路线的建议都将不胜感激!我环顾四周,在大多数情况下,我所看到的是使用 Begin/End 方法,我认为这种方法不能满足我想做的事情。
编辑
我正在考虑的另一种选择是使用注册方法,它需要等待回调。我遍历所有回调,获取它们的任务并使用 WhenAll
.
等待
private List<Func<Task>> ShutdownCallbacks = new List<Func<Task>>();
public void RegisterShutdownCallback(Func<Task> callback)
{
this.ShutdownCallbacks.Add(callback);
}
public async Task Shutdown()
{
var callbackTasks = new List<Task>();
foreach(var callback in this.ShutdownCallbacks)
{
callbackTasks.Add(callback());
}
await Task.WhenAll(callbackTasks);
}
的确如此,事件本质上是不可等待的,因此您必须解决它。
我过去使用的一个解决方案是使用 a semaphore 等待其中的所有条目被释放。在我的情况下,我只有一个订阅的事件,所以我可以将它硬编码为 new SemaphoreSlim(0, 1)
但在你的情况下,你可能想为你的事件覆盖 getter/setter 并记录有多少订阅者,所以你可以动态设置并发线程的最大数量。
之后,您将一个信号量条目传递给每个订阅者并让他们做他们的事情,直到 SemaphoreSlim.CurrentCount == amountOfSubscribers
(又名:所有点都已被释放)。
这实际上会阻止您的程序,直到所有事件订阅者都完成。
您可能还想考虑为您的订阅者提供一个类似 GameShutDownFinished
的事件,他们必须在完成游戏结束任务时调用该事件。结合 SemaphoreSlim.Release(int)
重载,您现在可以清除所有信号量条目并简单地使用 Semaphore.Wait()
来阻塞线程。您现在不必检查是否所有条目都已被清除,而是等到一个点被释放(但应该只有一个时刻所有点被一次释放)。
就我个人而言,我认为拥有 async
事件处理程序可能不是最佳设计选择,其中最重要的原因就是您遇到的问题。使用同步处理程序,知道它们何时完成是微不足道的。
就是说,如果出于某种原因您必须或至少被迫坚持使用此设计,您可以以 await
友好的方式进行。
您注册处理程序的想法 await
他们是个好主意。但是,我建议坚持使用现有的事件范例,因为这将保持代码中事件的表现力。最主要的是你必须偏离标准的基于 EventHandler
的委托类型,并使用 returns 和 Task
的委托类型,这样你就可以 await
处理程序.
这里有一个简单的例子来说明我的意思:
class A
{
public event Func<object, EventArgs, Task> Shutdown;
public async Task OnShutdown()
{
Func<object, EventArgs, Task> handler = Shutdown;
if (handler == null)
{
return;
}
Delegate[] invocationList = handler.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, EventArgs, Task>)invocationList[i])(this, EventArgs.Empty);
}
await Task.WhenAll(handlerTasks);
}
}
OnShutdown()
方法,在完成标准"get local copy of the event delegate instance"之后,首先调用所有的处理程序,然后等待所有返回的Tasks
(已将它们保存到本地数组当处理程序被调用时)。
下面是一个简短的控制台程序,说明了用法:
class Program
{
static void Main(string[] args)
{
A a = new A();
a.Shutdown += Handler1;
a.Shutdown += Handler2;
a.Shutdown += Handler3;
a.OnShutdown().Wait();
}
static async Task Handler1(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #1");
await Task.Delay(1000);
Console.WriteLine("Done with shutdown handler #1");
}
static async Task Handler2(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #2");
await Task.Delay(5000);
Console.WriteLine("Done with shutdown handler #2");
}
static async Task Handler3(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #3");
await Task.Delay(2000);
Console.WriteLine("Done with shutdown handler #3");
}
}
看完这个例子后,我现在发现自己想知道 C# 是否有办法对此进行一点抽象。也许这样的改变太复杂了,但是当前混合使用旧式 void
-返回事件处理程序和新的 async
/await
功能似乎有点尴尬。上面的工作(并且工作得很好,恕我直言),但是如果有更好的 CLR and/or 语言支持场景(即能够等待多播委托并让 C# 编译器将其转换为调用至 WhenAll()
).
我知道该操作专门询问有关为此使用异步和任务的问题,但这里有一个替代方案,这意味着处理程序不需要 return 一个值。该代码基于 Peter Duniho 的示例。首先是等效的 class A(稍微压扁以适应):-
class A
{
public delegate void ShutdownEventHandler(EventArgs e);
public event ShutdownEventHandler ShutdownEvent;
public void OnShutdownEvent(EventArgs e)
{
ShutdownEventHandler handler = ShutdownEvent;
if (handler == null) { return; }
Delegate[] invocationList = handler.GetInvocationList();
Parallel.ForEach<Delegate>(invocationList,
(hndler) => { ((ShutdownEventHandler)hndler)(e); });
}
}
一个简单的控制台应用程序来展示它的用途...
using System;
using System.Threading;
using System.Threading.Tasks;
...
class Program
{
static void Main(string[] args)
{
A a = new A();
a.ShutdownEvent += Handler1;
a.ShutdownEvent += Handler2;
a.ShutdownEvent += Handler3;
a.OnShutdownEvent(new EventArgs());
Console.WriteLine("Handlers should all be done now.");
Console.ReadKey();
}
static void handlerCore( int id, int offset, int num )
{
Console.WriteLine("Starting shutdown handler #{0}", id);
int step = 200;
Thread.Sleep(offset);
for( int i = 0; i < num; i += step)
{
Thread.Sleep(step);
Console.WriteLine("...Handler #{0} working - {1}/{2}", id, i, num);
}
Console.WriteLine("Done with shutdown handler #{0}", id);
}
static void Handler1(EventArgs e) { handlerCore(1, 7, 5000); }
static void Handler2(EventArgs e) { handlerCore(2, 5, 3000); }
static void Handler3(EventArgs e) { handlerCore(3, 3, 1000); }
}
我希望这对某人有用。
internal static class EventExtensions
{
public static void InvokeAsync<TEventArgs>(this EventHandler<TEventArgs> @event, object sender,
TEventArgs args, AsyncCallback ar, object userObject = null)
where TEventArgs : class
{
var listeners = @event.GetInvocationList();
foreach (var t in listeners)
{
var handler = (EventHandler<TEventArgs>) t;
handler.BeginInvoke(sender, args, ar, userObject);
}
}
}
示例:
public event EventHandler<CodeGenEventArgs> CodeGenClick;
private void CodeGenClickAsync(CodeGenEventArgs args)
{
CodeGenClick.InvokeAsync(this, args, ar =>
{
InvokeUI(() =>
{
if (args.Code.IsNotNullOrEmpty())
{
var oldValue = (string) gv.GetRowCellValue(gv.FocusedRowHandle, nameof(License.Code));
if (oldValue != args.Code)
gv.SetRowCellValue(gv.FocusedRowHandle, nameof(License.Code), args.Code);
}
});
});
}
注意:这是异步的,因此事件处理程序可能会危及 UI 线程。事件处理程序(订阅者)不应该做 UI-work。
否则就没有多大意义了。
在您的活动提供商中声明您的活动:
public 事件 EventHandler DoSomething;
调用您的提供商的事件:
DoSomething.InvokeAsync(new MyEventArgs(), this, ar => { 完成时调用的回调(此处需要时同步 UI!)}, null);
像往常一样由客户订阅活动
示例很棒,我只是使用 LINQ 和扩展对其进行了一些简化:
public static class AsynchronousEventExtensions
{
public static Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
where TEventArgs : EventArgs
{
if (handlers != null)
{
return Task.WhenAll(handlers.GetInvocationList()
.OfType<Func<TSource, TEventArgs, Task>>()
.Select(h => h(source, args)));
}
return Task.CompletedTask;
}
}
添加超时可能是个好主意。要引发事件调用 Raise 扩展:
public event Func<A, EventArgs, Task> Shutdown;
private async Task SomeMethod()
{
...
await Shutdown.Raise(this, EventArgs.Empty);
...
}
但您必须意识到,与同步事件不同,此实现会同时调用处理程序。如果处理程序必须严格连续地执行它们经常执行的操作,这可能是一个问题,例如下一个处理程序取决于前一个处理程序的结果:
someInstance.Shutdown += OnShutdown1;
someInstance.Shutdown += OnShutdown2;
...
private async Task OnShutdown1(SomeClass source, MyEventArgs args)
{
if (!args.IsProcessed)
{
// An operation
await Task.Delay(123);
args.IsProcessed = true;
}
}
private async Task OnShutdown2(SomeClass source, MyEventArgs args)
{
// OnShutdown2 will start execution the moment OnShutdown1 hits await
// and will proceed to the operation, which is not the desired behavior.
// Or it can be just a concurrent DB query using the same connection
// which can result in an exception thrown base on the provider
// and connection string options
if (!args.IsProcessed)
{
// An operation
await Task.Delay(123);
args.IsProcessed = true;
}
}
您最好将扩展方法更改为连续调用处理程序:
public static class AsynchronousEventExtensions
{
public static async Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
where TEventArgs : EventArgs
{
if (handlers != null)
{
foreach (Func<TSource, TEventArgs, Task> handler in handlers.GetInvocationList())
{
await handler(source, args);
}
}
}
}
如果您需要等待标准的 .net 事件处理程序,您不能这样做,因为它是 void
。
但是您可以创建一个异步事件系统来处理:
public delegate Task AsyncEventHandler(AsyncEventArgs e);
public class AsyncEventArgs : System.EventArgs
{
public bool Handled { get; set; }
}
public class AsyncEvent
{
private string name;
private List<AsyncEventHandler> handlers;
private Action<string, Exception> errorHandler;
public AsyncEvent(string name, Action<string, Exception> errorHandler)
{
this.name = name;
this.handlers = new List<AsyncEventHandler>();
this.errorHandler = errorHandler;
}
public void Register(AsyncEventHandler handler)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (this.handlers)
this.handlers.Add(handler);
}
public void Unregister(AsyncEventHandler handler)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (this.handlers)
this.handlers.Remove(handler);
}
public IReadOnlyList<AsyncEventHandler> Handlers
{
get
{
var temp = default(AsyncEventHandler[]);
lock (this.handlers)
temp = this.handlers.ToArray();
return temp.ToList().AsReadOnly();
}
}
public async Task InvokeAsync()
{
var ev = new AsyncEventArgs();
var exceptions = new List<Exception>();
foreach (var handler in this.Handlers)
{
try
{
await handler(ev).ConfigureAwait(false);
if (ev.Handled)
break;
}
catch(Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any())
this.errorHandler?.Invoke(this.name, new AggregateException(exceptions));
}
}
你现在可以声明你的异步事件了:
public class MyGame
{
private AsyncEvent _gameShuttingDown;
public event AsyncEventHandler GameShuttingDown
{
add => this._gameShuttingDown.Register(value);
remove => this._gameShuttingDown.Unregister(value);
}
void ErrorHandler(string name, Exception ex)
{
// handle event error.
}
public MyGame()
{
this._gameShuttingDown = new AsyncEvent("GAME_SHUTTING_DOWN", this.ErrorHandler);.
}
}
并使用以下方式调用您的异步事件:
internal async Task NotifyGameShuttingDownAsync()
{
await this._gameShuttingDown.InvokeAsync().ConfigureAwait(false);
}
通用版本:
public delegate Task AsyncEventHandler<in T>(T e) where T : AsyncEventArgs;
public class AsyncEvent<T> where T : AsyncEventArgs
{
private string name;
private List<AsyncEventHandler<T>> handlers;
private Action<string, Exception> errorHandler;
public AsyncEvent(string name, Action<string, Exception> errorHandler)
{
this.name = name;
this.handlers = new List<AsyncEventHandler<T>>();
this.errorHandler = errorHandler;
}
public void Register(AsyncEventHandler<T> handler)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (this.handlers)
this.handlers.Add(handler);
}
public void Unregister(AsyncEventHandler<T> handler)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (this.handlers)
this.handlers.Remove(handler);
}
public IReadOnlyList<AsyncEventHandler<T>> Handlers
{
get
{
var temp = default(AsyncEventHandler<T>[]);
lock (this.handlers)
temp = this.handlers.ToArray();
return temp.ToList().AsReadOnly();
}
}
public async Task InvokeAsync(T ev)
{
var exceptions = new List<Exception>();
foreach (var handler in this.Handlers)
{
try
{
await handler(ev).ConfigureAwait(false);
if (ev.Handled)
break;
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any())
this.errorHandler?.Invoke(this.name, new AggregateException(exceptions));
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Example
{
// delegate as alternative standard EventHandler
public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e, CancellationToken token);
public class ExampleObject
{
// use as regular event field
public event AsyncEventHandler<EventArgs> AsyncEvent;
// invoke using the extension method
public async Task InvokeEventAsync(CancellationToken token) {
await this.AsyncEvent.InvokeAsync(this, EventArgs.Empty, token);
}
// subscribe (add a listener) with regular syntax
public static async Task UsageAsync() {
var item = new ExampleObject();
item.AsyncEvent += (sender, e, token) => Task.CompletedTask;
await item.InvokeEventAsync(CancellationToken.None);
}
}
public static class AsynEventHandlerExtensions
{
// invoke a async event (with null-checking)
public static async Task InvokeAsync<TEventArgs>(this AsyncEventHandler<TEventArgs> handler, object sender, TEventArgs args, CancellationToken token) {
var delegates = handler?.GetInvocationList();
if (delegates?.Length > 0) {
var tasks = delegates
.Cast<AsyncEventHandler<TEventArgs>>()
.Select(e => e.Invoke(sender, args, token));
await Task.WhenAll(tasks);
}
}
}
}
我正在创建一个包含一系列事件的 class,其中之一是 GameShuttingDown
。触发此事件时,我需要调用事件处理程序。此事件的目的是通知用户游戏正在关闭,他们需要保存数据。保存是可等待的,而事件不是。因此,当处理程序被调用时,游戏会在等待的处理程序完成之前关闭。
public event EventHandler<EventArgs> GameShuttingDown;
public virtual async Task ShutdownGame()
{
await this.NotifyGameShuttingDown();
await this.SaveWorlds();
this.NotifyGameShutDown();
}
private async Task SaveWorlds()
{
foreach (DefaultWorld world in this.Worlds)
{
await this.worldService.SaveWorld(world);
}
}
protected virtual void NotifyGameShuttingDown()
{
var handler = this.GameShuttingDown;
if (handler == null)
{
return;
}
handler(this, new EventArgs());
}
活动报名
// The game gets shut down before this completes because of the nature of how events work
DefaultGame.GameShuttingDown += async (sender, args) => await this.repo.Save(blah);
我知道事件的签名是 void EventName
所以让它异步基本上是即发即忘。我的引擎大量使用事件来通知 3rd 方开发人员(和多个内部组件)引擎内正在发生事件并让他们对它们做出反应。
是否有好的途径可以用我可以使用的基于异步的东西替换事件?我不确定我是否应该将 BeginShutdownGame
和 EndShutdownGame
与回调一起使用,但这很痛苦,因为只有调用源可以传递回调,而不是任何插入到引擎,这是我从事件中得到的。如果服务器调用 game.ShutdownGame()
,引擎插件和/或引擎中的其他组件无法传递它们的回调,除非我连接某种注册方法,保留回调集合。
任何关于 preferred/recommended 路线的建议都将不胜感激!我环顾四周,在大多数情况下,我所看到的是使用 Begin/End 方法,我认为这种方法不能满足我想做的事情。
编辑
我正在考虑的另一种选择是使用注册方法,它需要等待回调。我遍历所有回调,获取它们的任务并使用 WhenAll
.
private List<Func<Task>> ShutdownCallbacks = new List<Func<Task>>();
public void RegisterShutdownCallback(Func<Task> callback)
{
this.ShutdownCallbacks.Add(callback);
}
public async Task Shutdown()
{
var callbackTasks = new List<Task>();
foreach(var callback in this.ShutdownCallbacks)
{
callbackTasks.Add(callback());
}
await Task.WhenAll(callbackTasks);
}
的确如此,事件本质上是不可等待的,因此您必须解决它。
我过去使用的一个解决方案是使用 a semaphore 等待其中的所有条目被释放。在我的情况下,我只有一个订阅的事件,所以我可以将它硬编码为 new SemaphoreSlim(0, 1)
但在你的情况下,你可能想为你的事件覆盖 getter/setter 并记录有多少订阅者,所以你可以动态设置并发线程的最大数量。
之后,您将一个信号量条目传递给每个订阅者并让他们做他们的事情,直到 SemaphoreSlim.CurrentCount == amountOfSubscribers
(又名:所有点都已被释放)。
这实际上会阻止您的程序,直到所有事件订阅者都完成。
您可能还想考虑为您的订阅者提供一个类似 GameShutDownFinished
的事件,他们必须在完成游戏结束任务时调用该事件。结合 SemaphoreSlim.Release(int)
重载,您现在可以清除所有信号量条目并简单地使用 Semaphore.Wait()
来阻塞线程。您现在不必检查是否所有条目都已被清除,而是等到一个点被释放(但应该只有一个时刻所有点被一次释放)。
就我个人而言,我认为拥有 async
事件处理程序可能不是最佳设计选择,其中最重要的原因就是您遇到的问题。使用同步处理程序,知道它们何时完成是微不足道的。
就是说,如果出于某种原因您必须或至少被迫坚持使用此设计,您可以以 await
友好的方式进行。
您注册处理程序的想法 await
他们是个好主意。但是,我建议坚持使用现有的事件范例,因为这将保持代码中事件的表现力。最主要的是你必须偏离标准的基于 EventHandler
的委托类型,并使用 returns 和 Task
的委托类型,这样你就可以 await
处理程序.
这里有一个简单的例子来说明我的意思:
class A
{
public event Func<object, EventArgs, Task> Shutdown;
public async Task OnShutdown()
{
Func<object, EventArgs, Task> handler = Shutdown;
if (handler == null)
{
return;
}
Delegate[] invocationList = handler.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, EventArgs, Task>)invocationList[i])(this, EventArgs.Empty);
}
await Task.WhenAll(handlerTasks);
}
}
OnShutdown()
方法,在完成标准"get local copy of the event delegate instance"之后,首先调用所有的处理程序,然后等待所有返回的Tasks
(已将它们保存到本地数组当处理程序被调用时)。
下面是一个简短的控制台程序,说明了用法:
class Program
{
static void Main(string[] args)
{
A a = new A();
a.Shutdown += Handler1;
a.Shutdown += Handler2;
a.Shutdown += Handler3;
a.OnShutdown().Wait();
}
static async Task Handler1(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #1");
await Task.Delay(1000);
Console.WriteLine("Done with shutdown handler #1");
}
static async Task Handler2(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #2");
await Task.Delay(5000);
Console.WriteLine("Done with shutdown handler #2");
}
static async Task Handler3(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #3");
await Task.Delay(2000);
Console.WriteLine("Done with shutdown handler #3");
}
}
看完这个例子后,我现在发现自己想知道 C# 是否有办法对此进行一点抽象。也许这样的改变太复杂了,但是当前混合使用旧式 void
-返回事件处理程序和新的 async
/await
功能似乎有点尴尬。上面的工作(并且工作得很好,恕我直言),但是如果有更好的 CLR and/or 语言支持场景(即能够等待多播委托并让 C# 编译器将其转换为调用至 WhenAll()
).
我知道该操作专门询问有关为此使用异步和任务的问题,但这里有一个替代方案,这意味着处理程序不需要 return 一个值。该代码基于 Peter Duniho 的示例。首先是等效的 class A(稍微压扁以适应):-
class A
{
public delegate void ShutdownEventHandler(EventArgs e);
public event ShutdownEventHandler ShutdownEvent;
public void OnShutdownEvent(EventArgs e)
{
ShutdownEventHandler handler = ShutdownEvent;
if (handler == null) { return; }
Delegate[] invocationList = handler.GetInvocationList();
Parallel.ForEach<Delegate>(invocationList,
(hndler) => { ((ShutdownEventHandler)hndler)(e); });
}
}
一个简单的控制台应用程序来展示它的用途...
using System;
using System.Threading;
using System.Threading.Tasks;
...
class Program
{
static void Main(string[] args)
{
A a = new A();
a.ShutdownEvent += Handler1;
a.ShutdownEvent += Handler2;
a.ShutdownEvent += Handler3;
a.OnShutdownEvent(new EventArgs());
Console.WriteLine("Handlers should all be done now.");
Console.ReadKey();
}
static void handlerCore( int id, int offset, int num )
{
Console.WriteLine("Starting shutdown handler #{0}", id);
int step = 200;
Thread.Sleep(offset);
for( int i = 0; i < num; i += step)
{
Thread.Sleep(step);
Console.WriteLine("...Handler #{0} working - {1}/{2}", id, i, num);
}
Console.WriteLine("Done with shutdown handler #{0}", id);
}
static void Handler1(EventArgs e) { handlerCore(1, 7, 5000); }
static void Handler2(EventArgs e) { handlerCore(2, 5, 3000); }
static void Handler3(EventArgs e) { handlerCore(3, 3, 1000); }
}
我希望这对某人有用。
internal static class EventExtensions
{
public static void InvokeAsync<TEventArgs>(this EventHandler<TEventArgs> @event, object sender,
TEventArgs args, AsyncCallback ar, object userObject = null)
where TEventArgs : class
{
var listeners = @event.GetInvocationList();
foreach (var t in listeners)
{
var handler = (EventHandler<TEventArgs>) t;
handler.BeginInvoke(sender, args, ar, userObject);
}
}
}
示例:
public event EventHandler<CodeGenEventArgs> CodeGenClick;
private void CodeGenClickAsync(CodeGenEventArgs args)
{
CodeGenClick.InvokeAsync(this, args, ar =>
{
InvokeUI(() =>
{
if (args.Code.IsNotNullOrEmpty())
{
var oldValue = (string) gv.GetRowCellValue(gv.FocusedRowHandle, nameof(License.Code));
if (oldValue != args.Code)
gv.SetRowCellValue(gv.FocusedRowHandle, nameof(License.Code), args.Code);
}
});
});
}
注意:这是异步的,因此事件处理程序可能会危及 UI 线程。事件处理程序(订阅者)不应该做 UI-work。 否则就没有多大意义了。
在您的活动提供商中声明您的活动:
public 事件 EventHandler DoSomething;
调用您的提供商的事件:
DoSomething.InvokeAsync(new MyEventArgs(), this, ar => { 完成时调用的回调(此处需要时同步 UI!)}, null);
像往常一样由客户订阅活动
public static class AsynchronousEventExtensions
{
public static Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
where TEventArgs : EventArgs
{
if (handlers != null)
{
return Task.WhenAll(handlers.GetInvocationList()
.OfType<Func<TSource, TEventArgs, Task>>()
.Select(h => h(source, args)));
}
return Task.CompletedTask;
}
}
添加超时可能是个好主意。要引发事件调用 Raise 扩展:
public event Func<A, EventArgs, Task> Shutdown;
private async Task SomeMethod()
{
...
await Shutdown.Raise(this, EventArgs.Empty);
...
}
但您必须意识到,与同步事件不同,此实现会同时调用处理程序。如果处理程序必须严格连续地执行它们经常执行的操作,这可能是一个问题,例如下一个处理程序取决于前一个处理程序的结果:
someInstance.Shutdown += OnShutdown1;
someInstance.Shutdown += OnShutdown2;
...
private async Task OnShutdown1(SomeClass source, MyEventArgs args)
{
if (!args.IsProcessed)
{
// An operation
await Task.Delay(123);
args.IsProcessed = true;
}
}
private async Task OnShutdown2(SomeClass source, MyEventArgs args)
{
// OnShutdown2 will start execution the moment OnShutdown1 hits await
// and will proceed to the operation, which is not the desired behavior.
// Or it can be just a concurrent DB query using the same connection
// which can result in an exception thrown base on the provider
// and connection string options
if (!args.IsProcessed)
{
// An operation
await Task.Delay(123);
args.IsProcessed = true;
}
}
您最好将扩展方法更改为连续调用处理程序:
public static class AsynchronousEventExtensions
{
public static async Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
where TEventArgs : EventArgs
{
if (handlers != null)
{
foreach (Func<TSource, TEventArgs, Task> handler in handlers.GetInvocationList())
{
await handler(source, args);
}
}
}
}
如果您需要等待标准的 .net 事件处理程序,您不能这样做,因为它是 void
。
但是您可以创建一个异步事件系统来处理:
public delegate Task AsyncEventHandler(AsyncEventArgs e);
public class AsyncEventArgs : System.EventArgs
{
public bool Handled { get; set; }
}
public class AsyncEvent
{
private string name;
private List<AsyncEventHandler> handlers;
private Action<string, Exception> errorHandler;
public AsyncEvent(string name, Action<string, Exception> errorHandler)
{
this.name = name;
this.handlers = new List<AsyncEventHandler>();
this.errorHandler = errorHandler;
}
public void Register(AsyncEventHandler handler)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (this.handlers)
this.handlers.Add(handler);
}
public void Unregister(AsyncEventHandler handler)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (this.handlers)
this.handlers.Remove(handler);
}
public IReadOnlyList<AsyncEventHandler> Handlers
{
get
{
var temp = default(AsyncEventHandler[]);
lock (this.handlers)
temp = this.handlers.ToArray();
return temp.ToList().AsReadOnly();
}
}
public async Task InvokeAsync()
{
var ev = new AsyncEventArgs();
var exceptions = new List<Exception>();
foreach (var handler in this.Handlers)
{
try
{
await handler(ev).ConfigureAwait(false);
if (ev.Handled)
break;
}
catch(Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any())
this.errorHandler?.Invoke(this.name, new AggregateException(exceptions));
}
}
你现在可以声明你的异步事件了:
public class MyGame
{
private AsyncEvent _gameShuttingDown;
public event AsyncEventHandler GameShuttingDown
{
add => this._gameShuttingDown.Register(value);
remove => this._gameShuttingDown.Unregister(value);
}
void ErrorHandler(string name, Exception ex)
{
// handle event error.
}
public MyGame()
{
this._gameShuttingDown = new AsyncEvent("GAME_SHUTTING_DOWN", this.ErrorHandler);.
}
}
并使用以下方式调用您的异步事件:
internal async Task NotifyGameShuttingDownAsync()
{
await this._gameShuttingDown.InvokeAsync().ConfigureAwait(false);
}
通用版本:
public delegate Task AsyncEventHandler<in T>(T e) where T : AsyncEventArgs;
public class AsyncEvent<T> where T : AsyncEventArgs
{
private string name;
private List<AsyncEventHandler<T>> handlers;
private Action<string, Exception> errorHandler;
public AsyncEvent(string name, Action<string, Exception> errorHandler)
{
this.name = name;
this.handlers = new List<AsyncEventHandler<T>>();
this.errorHandler = errorHandler;
}
public void Register(AsyncEventHandler<T> handler)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (this.handlers)
this.handlers.Add(handler);
}
public void Unregister(AsyncEventHandler<T> handler)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (this.handlers)
this.handlers.Remove(handler);
}
public IReadOnlyList<AsyncEventHandler<T>> Handlers
{
get
{
var temp = default(AsyncEventHandler<T>[]);
lock (this.handlers)
temp = this.handlers.ToArray();
return temp.ToList().AsReadOnly();
}
}
public async Task InvokeAsync(T ev)
{
var exceptions = new List<Exception>();
foreach (var handler in this.Handlers)
{
try
{
await handler(ev).ConfigureAwait(false);
if (ev.Handled)
break;
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any())
this.errorHandler?.Invoke(this.name, new AggregateException(exceptions));
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Example
{
// delegate as alternative standard EventHandler
public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e, CancellationToken token);
public class ExampleObject
{
// use as regular event field
public event AsyncEventHandler<EventArgs> AsyncEvent;
// invoke using the extension method
public async Task InvokeEventAsync(CancellationToken token) {
await this.AsyncEvent.InvokeAsync(this, EventArgs.Empty, token);
}
// subscribe (add a listener) with regular syntax
public static async Task UsageAsync() {
var item = new ExampleObject();
item.AsyncEvent += (sender, e, token) => Task.CompletedTask;
await item.InvokeEventAsync(CancellationToken.None);
}
}
public static class AsynEventHandlerExtensions
{
// invoke a async event (with null-checking)
public static async Task InvokeAsync<TEventArgs>(this AsyncEventHandler<TEventArgs> handler, object sender, TEventArgs args, CancellationToken token) {
var delegates = handler?.GetInvocationList();
if (delegates?.Length > 0) {
var tasks = delegates
.Cast<AsyncEventHandler<TEventArgs>>()
.Select(e => e.Invoke(sender, args, token));
await Task.WhenAll(tasks);
}
}
}
}