将事件转换为任务的可重用模式
我想有一个通用的可重用的代码包装EAP模式作为任务 ,类似于什么Task.Factory.FromAsync
的BeginXXX/EndXXX
APM模式 。
例如:
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); } } }
是否有可能想出类似的东西,但不需要我inputWebBrowserDocumentCompletedEventHandler
两次( registerEvent
/ unregisterEvent
),而不诉诸反思?
可以用一个助手类和stream利的语法:
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
帮助器方法,它可以从转换器参数中推断出types参数,这意味着你只需要写一次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);
从EAP转换到Tasks并不那么简单,主要是因为在调用长时间运行的方法和处理事件时都必须处理exception。
ParallelExtensionsExtras库包含EAPCommon.HandleCompletion(TaskCompletionSource tcs,AsyncCompletedEventArgs e,Func getResult,Action unregisterHandler)扩展方法,使转换更容易。 该方法处理从事件订阅/取消订阅。 它也不会尝试启动长时间运行的操作
使用此方法,该库实现SmtpClient,WebClient和PingClient的asynchronous版本。
以下方法显示了一般使用模式:
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;
扩展方法创build处理程序并挂接tcs。 您的代码将处理程序设置为源对象并启动长操作。 实际的处理程序types不会泄漏到方法之外。
通过分离两个问题(处理事件与开始操作),创build一个通用的方法更容易。
我觉得下面的版本可能会令人满意。 我没有借用从max的答案准备一个正确types的事件处理程序的想法,但是这个实现不会显式地创build任何额外的对象。
作为积极的副作用,它允许调用者根据事件的参数(如AsyncCompletedEventArgs.Cancelled
, AsyncCompletedEventArgs.Error
)取消或拒绝操作的结果(有例外)。
底层的TaskCompletionSource
对于调用者来说仍然是完全隐藏的(所以它可以被其他的东西替代,例如自定义的awaiter或自定义的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); } } }
这实际上可以用来等待任何callback,而不仅仅是事件处理程序,例如:
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);
我有一个(用法明智)更短的解决scheme。 我会先告诉你这个用法,然后给你这个代码(使用它)。
用法如:
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, eg 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; } }