Método del método FromEvent de propósito general

Usando el nuevo modelo async / await es bastante sencillo generar una Task que se completa cuando se activa un evento; solo necesitas seguir este patrón:

 public class MyClass { public event Action OnCompletion; } public static Task FromEvent(MyClass obj) { TaskCompletionSource tcs = new TaskCompletionSource(); obj.OnCompletion += () => { tcs.SetResult(null); }; return tcs.Task; } 

Esto luego permite:

 await FromEvent(new MyClass()); 

El problema es que necesita crear un nuevo método FromEvent para cada evento en cada clase que le gustaría await . Eso podría ser muy grande realmente rápido, y de todos modos es solo un código repetitivo.

Idealmente, me gustaría poder hacer algo como esto:

 await FromEvent(new MyClass().OnCompletion); 

Entonces podría volver a utilizar el mismo método FromEvent para cualquier evento en cualquier instancia. He pasado un tiempo tratando de crear un método así, y hay una serie de inconvenientes. Para el código anterior generará el siguiente error:

El evento ‘Namespace.MyClass.OnCompletion’ solo puede aparecer en el lado izquierdo de + = o – =

Por lo que puedo decir, nunca habrá una forma de pasar el evento así a través del código.

Entonces, la mejor alternativa parecía estar intentando pasar el nombre del evento como una cadena:

 await FromEvent(new MyClass(), "OnCompletion"); 

No es tan ideal; no obtiene intellisense y obtendría un error de tiempo de ejecución si el evento no existe para ese tipo, pero aún podría ser más útil que un montón de métodos FromEvent.

Por lo tanto, es bastante fácil usar reflection y GetEvent(eventName) para obtener el objeto EventInfo . El siguiente problema es que el delegado de ese evento no se conoce (y debe poder variar) en tiempo de ejecución. Eso hace que sea difícil agregar un controlador de eventos, porque necesitamos crear dinámicamente un método en tiempo de ejecución, haciendo coincidir una firma determinada (pero ignorando todos los parámetros) que accede a TaskCompletionSource que ya tenemos y establece su resultado.

Afortunadamente encontré este enlace que contiene instrucciones sobre cómo hacer [casi] exactamente eso a través de Reflection.Emit . Ahora el problema es que necesitamos emitir IL, y no tengo idea de cómo acceder a la instancia de tcs que tengo.

Debajo está el progreso que he hecho para terminar esto:

 public static Task FromEvent(this T obj, string eventName) { var tcs = new TaskCompletionSource(); var eventInfo = obj.GetType().GetEvent(eventName); Type eventDelegate = eventInfo.EventHandlerType; Type[] parameterTypes = GetDelegateParameterTypes(eventDelegate); DynamicMethod handler = new DynamicMethod("unnamed", null, parameterTypes); ILGenerator ilgen = handler.GetILGenerator(); //TODO ilgen.Emit calls go here Delegate dEmitted = handler.CreateDelegate(eventDelegate); eventInfo.AddEventHandler(obj, dEmitted); return tcs.Task; } 

¿Qué IL podría posiblemente emitir que me permita establecer el resultado de TaskCompletionSource ? O, alternativamente, ¿existe otro enfoque para crear un método que devuelva una Tarea para cualquier evento arbitrario de un tipo arbitrario?

Aqui tienes:

 internal class TaskCompletionSourceHolder { private readonly TaskCompletionSource m_tcs; internal object Target { get; set; } internal EventInfo EventInfo { get; set; } internal Delegate Delegate { get; set; } internal TaskCompletionSourceHolder(TaskCompletionSource tsc) { m_tcs = tsc; } private void SetResult(params object[] args) { // this method will be called from emitted IL // so we can set result here, unsubscribe from the event // or do whatever we want. // object[] args will contain arguments // passed to the event handler m_tcs.SetResult(args); EventInfo.RemoveEventHandler(Target, Delegate); } } public static class ExtensionMethods { private static Dictionary s_emittedHandlers = new Dictionary(); private static void GetDelegateParameterAndReturnTypes(Type delegateType, out List parameterTypes, out Type returnType) { if (delegateType.BaseType != typeof(MulticastDelegate)) throw new ArgumentException("delegateType is not a delegate"); MethodInfo invoke = delegateType.GetMethod("Invoke"); if (invoke == null) throw new ArgumentException("delegateType is not a delegate."); ParameterInfo[] parameters = invoke.GetParameters(); parameterTypes = new List(parameters.Length); for (int i = 0; i < parameters.Length; i++) parameterTypes.Add(parameters[i].ParameterType); returnType = invoke.ReturnType; } public static Task FromEvent(this T obj, string eventName) { var tcs = new TaskCompletionSource(); var tcsh = new TaskCompletionSourceHolder(tcs); EventInfo eventInfo = obj.GetType().GetEvent(eventName); Type eventDelegateType = eventInfo.EventHandlerType; DynamicMethod handler; if (!s_emittedHandlers.TryGetValue(eventDelegateType, out handler)) { Type returnType; List parameterTypes; GetDelegateParameterAndReturnTypes(eventDelegateType, out parameterTypes, out returnType); if (returnType != typeof(void)) throw new NotSupportedException(); Type tcshType = tcsh.GetType(); MethodInfo setResultMethodInfo = tcshType.GetMethod( "SetResult", BindingFlags.NonPublic | BindingFlags.Instance); // I'm going to create an instance-like method // so, first argument must an instance itself // ie TaskCompletionSourceHolder *this* parameterTypes.Insert(0, tcshType); Type[] parameterTypesAr = parameterTypes.ToArray(); handler = new DynamicMethod("unnamed", returnType, parameterTypesAr, tcshType); ILGenerator ilgen = handler.GetILGenerator(); // declare local variable of type object[] LocalBuilder arr = ilgen.DeclareLocal(typeof(object[])); // push array's size onto the stack ilgen.Emit(OpCodes.Ldc_I4, parameterTypesAr.Length - 1); // create an object array of the given size ilgen.Emit(OpCodes.Newarr, typeof(object)); // and store it in the local variable ilgen.Emit(OpCodes.Stloc, arr); // iterate thru all arguments except the zero one (ie *this*) // and store them to the array for (int i = 1; i < parameterTypesAr.Length; i++) { // push the array onto the stack ilgen.Emit(OpCodes.Ldloc, arr); // push the argument's index onto the stack ilgen.Emit(OpCodes.Ldc_I4, i - 1); // push the argument onto the stack ilgen.Emit(OpCodes.Ldarg, i); // check if it is of a value type // and perform boxing if necessary if (parameterTypesAr[i].IsValueType) ilgen.Emit(OpCodes.Box, parameterTypesAr[i]); // store the value to the argument's array ilgen.Emit(OpCodes.Stelem, typeof(object)); } // load zero-argument (ie *this*) onto the stack ilgen.Emit(OpCodes.Ldarg_0); // load the array onto the stack ilgen.Emit(OpCodes.Ldloc, arr); // call this.SetResult(arr); ilgen.Emit(OpCodes.Call, setResultMethodInfo); // and return ilgen.Emit(OpCodes.Ret); s_emittedHandlers.Add(eventDelegateType, handler); } Delegate dEmitted = handler.CreateDelegate(eventDelegateType, tcsh); tcsh.Target = obj; tcsh.EventInfo = eventInfo; tcsh.Delegate = dEmitted; eventInfo.AddEventHandler(obj, dEmitted); return tcs.Task; } } 

Este código funcionará para casi todos los eventos que devuelven el vacío (independientemente de la lista de parámetros).

Se puede mejorar para admitir cualquier valor de retorno si es necesario.

Puede ver la diferencia entre los métodos de Dax y los míos a continuación:

 static async void Run() { object[] result = await new MyClass().FromEvent("Fired"); Console.WriteLine(string.Join(", ", result.Select(arg => arg.ToString()).ToArray())); // 123, abcd } public class MyClass { public delegate void TwoThings(int x, string y); public MyClass() { new Thread(() => { Thread.Sleep(1000); Fired(123, "abcd"); }).Start(); } public event TwoThings Fired; } 

En resumen, mi código es compatible con cualquier tipo de tipo de delegado. No debe (y no necesita) especificarlo explícitamente como TaskFromEvent .

Esto le dará lo que necesita sin necesidad de hacer ningún ilgen, y mucho más simple. Funciona con cualquier tipo de delegates de eventos; solo tiene que crear un controlador diferente para cada número de parámetros en su evento delegado. Debajo están los manejadores que necesitarías para 0..2, que debería ser la gran mayoría de tus casos de uso. Extender a 3 y más es una copia y pega simple del método de 2 parámetros.

Esto también es más poderoso que el método ilgen porque puede usar cualquier valor creado por el evento en su patrón asíncrono.

 // Empty events (Action style) static Task TaskFromEvent(object target, string eventName) { var addMethod = target.GetType().GetEvent(eventName).GetAddMethod(); var delegateType = addMethod.GetParameters()[0].ParameterType; var tcs = new TaskCompletionSource(); var resultSetter = (Action)(() => tcs.SetResult(null)); var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke"); addMethod.Invoke(target, new object[] { d }); return tcs.Task; } // One-value events (Action style) static Task TaskFromEvent(object target, string eventName) { var addMethod = target.GetType().GetEvent(eventName).GetAddMethod(); var delegateType = addMethod.GetParameters()[0].ParameterType; var tcs = new TaskCompletionSource(); var resultSetter = (Action)tcs.SetResult; var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke"); addMethod.Invoke(target, new object[] { d }); return tcs.Task; } // Two-value events (Action or EventHandler style) static Task> TaskFromEvent(object target, string eventName) { var addMethod = target.GetType().GetEvent(eventName).GetAddMethod(); var delegateType = addMethod.GetParameters()[0].ParameterType; var tcs = new TaskCompletionSource>(); var resultSetter = (Action)((t1, t2) => tcs.SetResult(Tuple.Create(t1, t2))); var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke"); addMethod.Invoke(target, new object[] { d }); return tcs.Task; } 

El uso sería así. Como puede ver, aunque el evento esté definido en un delegado personalizado, aún funciona. Y puede capturar los valores estimados como una tupla.

 static async void Run() { var result = await TaskFromEvent(new MyClass(), "Fired"); Console.WriteLine(result); // (123, "abcd") } public class MyClass { public delegate void TwoThings(int x, string y); public MyClass() { new Thread(() => { Thread.Sleep(1000); Fired(123, "abcd"); }).Start(); } public event TwoThings Fired; } 

Aquí hay una función auxiliar que te permitirá escribir las funciones TaskFromEvent en una sola línea cada una, si los tres métodos anteriores son demasiado copiar y pegar para tus preferencias. Se debe dar crédito al máximo para simplificar lo que tenía originalmente.

Si está dispuesto a tener un método por tipo de delegado, puede hacer algo como:

 Task FromEvent(Action add) { var tcs = new TaskCompletionSource(); add(() => tcs.SetResult(true)); return tcs.Task; } 

Lo usarías como:

 await FromEvent(x => new MyClass().OnCompletion += x); 

Tenga en cuenta que de esta manera usted nunca se dará de baja del evento, eso puede o no ser un problema para usted.

Si está utilizando delegates generics, un método por cada tipo genérico es suficiente, no necesita uno para cada tipo concreto:

 Task FromEvent(Action> add) { var tcs = new TaskCompletionSource(); add(x => tcs.SetResult(x)); return tcs.Task; } 

Aunque la inferencia de tipos no funciona con eso, debe especificar explícitamente el parámetro de tipo (suponiendo que el tipo de OnCompletion es Action aquí):

 string s = await FromEvent(x => c.OnCompletion += x);