用于将事件转换为任务的可重用模式
本文关键字:模式 任务 事件 转换 用于 | 更新日期: 2023-09-27 17:55:39
我想有一个通用的可重用代码段来包装EAP模式作为任务,类似于Task.Factory.FromAsync
为APM模式所做的BeginXXX/EndXXX
。
例如:
private async void Form1_Load(object sender, EventArgs e)
{
await TaskExt.FromEvent<EventArgs>(
handler => this.webBrowser.DocumentCompleted +=
new WebBrowserDocumentCompletedEventHandler(handler),
() => this.webBrowser.Navigate("about:blank"),
handler => this.webBrowser.DocumentCompleted -=
new WebBrowserDocumentCompletedEventHandler(handler),
CancellationToken.None);
this.webBrowser.Document.InvokeScript("setTimeout",
new[] { "document.body.style.backgroundColor = 'yellow'", "1" });
}
到目前为止,它看起来像这样:
public static class TaskExt
{
public static async Task<TEventArgs> FromEvent<TEventArgs>(
Action<EventHandler<TEventArgs>> registerEvent,
Action action,
Action<EventHandler<TEventArgs>> unregisterEvent,
CancellationToken token)
{
var tcs = new TaskCompletionSource<TEventArgs>();
EventHandler<TEventArgs> handler = (sender, args) =>
tcs.TrySetResult(args);
registerEvent(handler);
try
{
using (token.Register(() => tcs.SetCanceled()))
{
action();
return await tcs.Task;
}
}
finally
{
unregisterEvent(handler);
}
}
}
是否有可能想出类似的东西,但不需要我输入两次WebBrowserDocumentCompletedEventHandler
(对于 registerEvent
/unregisterEvent
),而无需诉诸反思?
使用帮助程序类和类似流利的语法是可能的:
public static class TaskExt
{
public static EAPTask<TEventArgs, EventHandler<TEventArgs>> FromEvent<TEventArgs>()
{
var tcs = new TaskCompletionSource<TEventArgs>();
var handler = new EventHandler<TEventArgs>((s, e) => tcs.TrySetResult(e));
return new EAPTask<TEventArgs, EventHandler<TEventArgs>>(tcs, handler);
}
}
public sealed class EAPTask<TEventArgs, TEventHandler>
where TEventHandler : class
{
private readonly TaskCompletionSource<TEventArgs> _completionSource;
private readonly TEventHandler _eventHandler;
public EAPTask(
TaskCompletionSource<TEventArgs> completionSource,
TEventHandler eventHandler)
{
_completionSource = completionSource;
_eventHandler = eventHandler;
}
public EAPTask<TEventArgs, TOtherEventHandler> WithHandlerConversion<TOtherEventHandler>(
Converter<TEventHandler, TOtherEventHandler> converter)
where TOtherEventHandler : class
{
return new EAPTask<TEventArgs, TOtherEventHandler>(
_completionSource, converter(_eventHandler));
}
public async Task<TEventArgs> Start(
Action<TEventHandler> subscribe,
Action action,
Action<TEventHandler> unsubscribe,
CancellationToken cancellationToken)
{
subscribe(_eventHandler);
try
{
using(cancellationToken.Register(() => _completionSource.SetCanceled()))
{
action();
return await _completionSource.Task;
}
}
finally
{
unsubscribe(_eventHandler);
}
}
}
现在你有一个WithHandlerConversion
帮助程序方法,它可以从转换器参数推断类型参数,这意味着你只需要编写一次WebBrowserDocumentCompletedEventHandler
。用法:
await TaskExt
.FromEvent<WebBrowserDocumentCompletedEventArgs>()
.WithHandlerConversion(handler => new WebBrowserDocumentCompletedEventHandler(handler))
.Start(
handler => this.webBrowser.DocumentCompleted += handler,
() => this.webBrowser.Navigate(@"about:blank"),
handler => this.webBrowser.DocumentCompleted -= handler,
CancellationToken.None);
我有一个(用法明智)更短的解决方案。我将首先向您展示用法,然后为您提供实现此目的的代码(自由使用它)。
用法例如:
await button.EventAsync(nameof(button.Click));
或:
var specialEventArgs = await busniessObject.EventAsync(nameof(busniessObject.CustomerCreated));
或者对于需要以某种方式触发的事件:
var serviceResult = await service.EventAsync(()=> service.Start, nameof(service.Completed));
实现这一点的魔力(请注意它是 C# 7.1 语法,但可以通过添加几行轻松转换回较低的语言版本):
using System;
using System.Threading;
using System.Threading.Tasks;
namespace SpacemonsterIndustries.Core
{
public static class EventExtensions
{
/// <summary>
/// Extension Method that converts a typical EventArgs Event into an awaitable Task
/// </summary>
/// <typeparam name="TEventArgs">The type of the EventArgs (must inherit from EventArgs)</typeparam>
/// <param name="objectWithEvent">the object that has the event</param>
/// <param name="trigger">optional Function that triggers the event</param>
/// <param name="eventName">the name of the event -> use nameof to be safe, e.g. nameof(button.Click) </param>
/// <param name="ct">an optional Cancellation Token</param>
/// <returns></returns>
public static async Task<TEventArgs> EventAsync<TEventArgs>(this object objectWithEvent, Action trigger, string eventName, CancellationToken ct = default)
where TEventArgs : EventArgs
{
var completionSource = new TaskCompletionSource<TEventArgs>(ct);
var eventInfo = objectWithEvent.GetType().GetEvent(eventName);
var delegateDef = new UniversalEventDelegate<TEventArgs>(Handler);
var handlerAsDelegate = Delegate.CreateDelegate(eventInfo.EventHandlerType, delegateDef.Target, delegateDef.Method);
eventInfo.AddEventHandler(objectWithEvent, handlerAsDelegate);
trigger?.Invoke();
var result = await completionSource.Task;
eventInfo.RemoveEventHandler(objectWithEvent, handlerAsDelegate);
return result;
void Handler(object sender, TEventArgs e) => completionSource.SetResult(e);
}
public static Task<TEventArgs> EventAsync<TEventArgs>(this object objectWithEvent, string eventName, CancellationToken ct = default) where TEventArgs : EventArgs
=> EventAsync<TEventArgs>(objectWithEvent, null, eventName, ct);
private delegate void UniversalEventDelegate<in TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;
}
}
以下版本可能足够令人满意。我确实从 max 的答案中借用了准备正确类型的事件处理程序的想法,但此实现不会显式创建任何其他对象。
作为积极的副作用,它允许调用方根据事件的参数(如AsyncCompletedEventArgs.Cancelled
、AsyncCompletedEventArgs.Error
)取消或拒绝操作的结果(例外)。
底层TaskCompletionSource
仍然对调用者完全隐藏(因此可以用其他东西替换,例如自定义等待者或自定义承诺):
private async void Form1_Load(object sender, EventArgs e)
{
await TaskExt.FromEvent<WebBrowserDocumentCompletedEventHandler, EventArgs>(
getHandler: (completeAction, cancelAction, rejectAction) =>
(eventSource, eventArgs) => completeAction(eventArgs),
subscribe: eventHandler =>
this.webBrowser.DocumentCompleted += eventHandler,
unsubscribe: eventHandler =>
this.webBrowser.DocumentCompleted -= eventHandler,
initiate: (completeAction, cancelAction, rejectAction) =>
this.webBrowser.Navigate("about:blank"),
token: CancellationToken.None);
this.webBrowser.Document.InvokeScript("setTimeout",
new[] { "document.body.style.backgroundColor = 'yellow'", "1" });
}
public static class TaskExt
{
public static async Task<TEventArgs> FromEvent<TEventHandler, TEventArgs>(
Func<Action<TEventArgs>, Action, Action<Exception>, TEventHandler> getHandler,
Action<TEventHandler> subscribe,
Action<TEventHandler> unsubscribe,
Action<Action<TEventArgs>, Action, Action<Exception>> initiate,
CancellationToken token = default) where TEventHandler : Delegate
{
var tcs = new TaskCompletionSource<TEventArgs>();
Action<TEventArgs> complete = args => tcs.TrySetResult(args);
Action cancel = () => tcs.TrySetCanceled();
Action<Exception> reject = ex => tcs.TrySetException(ex);
TEventHandler handler = getHandler(complete, cancel, reject);
subscribe(handler);
try
{
using (token.Register(() => tcs.TrySetCanceled(),
useSynchronizationContext: false))
{
initiate(complete, cancel, reject);
return await tcs.Task;
}
}
finally
{
unsubscribe(handler);
}
}
}
这实际上可以用来等待任何回调,而不仅仅是事件处理程序,例如:
var mre = new ManualResetEvent(false);
RegisteredWaitHandle rwh = null;
await TaskExt.FromEvent<WaitOrTimerCallback, bool>(
(complete, cancel, reject) =>
(state, timeout) => { if (!timeout) complete(true); else cancel(); },
callback =>
rwh = ThreadPool.RegisterWaitForSingleObject(mre, callback, null, 1000, true),
callback =>
rwh.Unregister(mre),
(complete, cancel, reject) =>
ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(500); mre.Set(); }),
CancellationToken.None);
更新了简单事件案例的更少样板(这些天我更频繁地使用这个):
public static async Task<TEventArgs> FromEvent<TEventHandler, TEventArgs>(
Action<TEventHandler> subscribe,
Action<TEventHandler> unsubscribe,
CancellationToken token = default,
bool runContinuationsAsynchronously = true)
where TEventHandler : Delegate
where TEventArgs: EventArgs
{
var tcs = new TaskCompletionSource<TEventArgs>(runContinuationsAsynchronously ?
TaskCreationOptions.RunContinuationsAsynchronously :
TaskCreationOptions.None);
var handler = new Action<object?, TEventArgs>((_, args) => tcs.TrySetResult(args));
var h = (TEventHandler)Delegate.CreateDelegate(typeof(TEventHandler), handler.Target, handler.Method);
subscribe(h);
try
{
using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false))
{
return await tcs.Task;
}
}
finally
{
unsubscribe(h);
}
}
用法:
await TaskExt.FromEvent<FormClosedEventHandler, FormClosedEventArgs>(
h => mainForm.FormClosed += h,
h => mainForm.FormClosed -= h,
token);
从 EAP 转换为任务并不是那么简单,主要是因为在调用长时间运行的方法和处理事件时都必须处理异常。
ParallelExtensionsExtras 库包含 EAPCommon.HandleCompletion(TaskCompletionSource tcs, AsyncCompletedEventArgs e, Func getResult, Action unregisterHandler) 扩展方法,使转换更容易。该方法处理事件的订阅/取消订阅。它也不会尝试启动长时间运行的操作
使用此方法,库实现了SmtpClient,WebClient和PingClient的异步版本。
以下方法显示了常规使用模式:
private static Task<PingReply> SendTaskCore(Ping ping, object userToken, Action<TaskCompletionSource<PingReply>> sendAsync)
{
// Validate we're being used with a real smtpClient. The rest of the arg validation
// will happen in the call to sendAsync.
if (ping == null) throw new ArgumentNullException("ping");
// Create a TaskCompletionSource to represent the operation
var tcs = new TaskCompletionSource<PingReply>(userToken);
// Register a handler that will transfer completion results to the TCS Task
PingCompletedEventHandler handler = null;
handler = (sender, e) => EAPCommon.HandleCompletion(tcs, e, () => e.Reply, () => ping.PingCompleted -= handler);
ping.PingCompleted += handler;
// Try to start the async operation. If starting it fails (due to parameter validation)
// unregister the handler before allowing the exception to propagate.
try
{
sendAsync(tcs);
}
catch(Exception exc)
{
ping.PingCompleted -= handler;
tcs.TrySetException(exc);
}
// Return the task to represent the asynchronous operation
return tcs.Task;
}
与您的代码的主要区别在于:
// Register a handler that will transfer completion results to the TCS Task
PingCompletedEventHandler handler = null;
handler = (sender, e) => EAPCommon.HandleCompletion(tcs, e, () => e.Reply,
() => ping.PingCompleted -= handler);
ping.PingCompleted += handler;
扩展方法创建处理程序并挂钩 tcs。代码将处理程序设置为源对象并启动长操作。实际处理程序类型不会泄漏到方法之外。
通过分离这两个关注点(处理事件与启动操作),可以更轻松地创建泛型方法。
这是一个最小使用反射的解决方案,灵感来自Observable.FromEvent
方法(反应式扩展)。
public static Task<TEventArgs> TaskFromEvent<TDelegate, TEventArgs>(
Action<TDelegate> addHandler, Action<TDelegate> removeHandler)
where TDelegate : Delegate where TEventArgs : EventArgs
{
var tcs = new TaskCompletionSource<TEventArgs>();
TDelegate specificHandler = null;
Action<object, TEventArgs> handler = (sender, e) =>
{
removeHandler(specificHandler);
handler = null;
tcs.SetResult(e);
tcs = null;
};
var invokeMethodInfo = typeof(Action<object, TEventArgs>).GetMethod("Invoke");
specificHandler = (TDelegate)invokeMethodInfo
.CreateDelegate(typeof(TDelegate), handler);
addHandler(specificHandler);
return tcs.Task;
}
使用示例:
var documentCompletedAsync = TaskFromEvent<
WebBrowserDocumentCompletedEventHandler,
WebBrowserDocumentCompletedEventArgs>(
handler => webBrowser.DocumentCompleted += handler,
handler => webBrowser.DocumentCompleted -= handler);
webBrowser.Navigate("about:blank");
var url = (await documentCompletedAsync).Url;