我想为wrapping EAP pattern as task提供一个通用的可重复使用的代码,类似于Task.Factory.FromAsync
对BeginXXX/EndXXX
APM pattern的代码。
E.g:
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
),求助于反思?
答案 0 :(得分:8)
可以使用辅助类和流畅的语法:
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
辅助方法,它可以从converter参数中推断出类型参数,这意味着你只需要写一次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);
答案 1 :(得分:4)
我有一个(使用明智的)更短的解决方案。我将首先向您展示使用情况,然后为您提供实现此目的的代码(自由使用) 用法例如:
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;
}
}
答案 2 :(得分:3)
从EAP转换为Tasks并不是那么简单,主要是因为在调用长时间运行的方法和处理事件时都必须处理异常。
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。您的代码将处理程序设置为源对象并启动长操作。实际的处理程序类型不会泄漏到方法之外。
通过分离两个问题(处理事件与开始操作),可以更容易地创建通用方法。
答案 3 :(得分:2)
我认为以下版本可能足够令人满意。我确实借用了从max's answer准备正确类型的事件处理程序的想法,但是这个实现并没有明确地创建任何其他对象。
作为一个积极的副作用,它允许调用者根据事件的参数(如AsyncCompletedEventArgs.Cancelled
,AsyncCompletedEventArgs.Error
取消或拒绝操作的结果(例外) )。
基础TaskCompletionSource
仍然完全隐藏在调用方之外(因此可以将其替换为其他内容,例如a custom awaiter或a custom promise):
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(CancellationToken)) where TEventHandler : class
{
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);
答案 4 :(得分:1)
这是一种受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;