2012-10-12 52 views
7

Usando el nuevo modelo async/await es bastante sencillo generar un Task que se completa cuando se produce un evento; sólo tiene que seguir este patrón:De uso general Método FromEvent

public class MyClass 
{ 
    public event Action OnCompletion; 
} 

public static Task FromEvent(MyClass obj) 
{ 
    TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(); 

    obj.OnCompletion +=() => 
     { 
      tcs.SetResult(null); 
     }; 

    return tcs.Task; 
} 

Esto permite:

await FromEvent(new MyClass()); 

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

Idealmente me gustaría ser capaz de 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:

The event 'Namespace.MyClass.OnCompletion' can only appear on the left hand side of += or -=

Por lo que yo puedo decir, no habrá nunca una forma de pasar el evento como este a través de código.

Por lo tanto, la siguiente mejor cosa parecía estar tratando de pasar el nombre del evento como una cadena:

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

No es tan ideales; 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 utilizar el reflejo 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, que coincida con una firma determinada (pero ignorando todos los parámetros) que tiene acceso a un TaskCompletionSource que ya tenemos y establece su resultado.

Afortunadamente encontré this link 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 tcs que tengo.

A continuación se muestra el progreso que he hecho a terminar esto:

public static Task FromEvent<T>(this T obj, string eventName) 
{ 
    var tcs = new TaskCompletionSource<object>(); 
    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; 
} 

Lo IL podría yo emitir que permitiría que fije el resultado de la TaskCompletionSource? O, alternativamente, ¿existe otro enfoque para crear un método que devuelva una Tarea para cualquier evento arbitrario de un tipo arbitrario?

+2

Tenga en cuenta que el BCL tiene 'TaskFactory.FromAsync' para traducir fácilmente de APM a TAP. No hay una manera fácil * y * de traducir de EAP a TAP, así que creo que es por eso que MS no incluyó una solución como esta. Encuentro que Rx (o TPL Dataflow) está más cerca de la semántica de "eventos" de todos modos, y Rx * does * tiene un tipo de método 'FromEvent'. –

+1

También quería crear un 'FromEvent <>' genérico, y [this] (http://stackoverflow.com/a/22798789/1768303) está más cerca de lo que podría llegar sin usar el reflejo. – Noseratio

Respuesta

21

Aquí van:

internal class TaskCompletionSourceHolder 
{ 
    private readonly TaskCompletionSource<object[]> m_tcs; 

    internal object Target { get; set; } 
    internal EventInfo EventInfo { get; set; } 
    internal Delegate Delegate { get; set; } 

    internal TaskCompletionSourceHolder(TaskCompletionSource<object[]> 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<Type, DynamicMethod> s_emittedHandlers = 
     new Dictionary<Type, DynamicMethod>(); 

    private static void GetDelegateParameterAndReturnTypes(Type delegateType, 
     out List<Type> 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<Type>(parameters.Length); 
     for (int i = 0; i < parameters.Length; i++) 
      parameterTypes.Add(parameters[i].ParameterType); 

     returnType = invoke.ReturnType; 
    } 

    public static Task<object[]> FromEvent<T>(this T obj, string eventName) 
    { 
     var tcs = new TaskCompletionSource<object[]>(); 
     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<Type> 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 
      // i.e. 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 (i.e. *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 (i.e. *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 void (independientemente de la lista de parámetros).

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

se puede ver la diferencia entre Dax y métodos de la mina 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 pocas palabras, mi código es compatible con realmente cualquier tipo de tipo de delegado. No debe (y no necesita) especificarlo explícitamente como TaskFromEvent<int, string>.

+0

Acabo de revisar su actualización y jugar con ella un poco. Realmente estoy Me gusta El controlador de eventos se anuló, lo cual es un gran toque. Los diversos manejadores de eventos están en caché, por lo que IL no se genera repetidamente para los mismos tipos y, a diferencia de otras soluciones, no hay necesidad de especificar los tipos de argumentos. al controlador de eventos. – Servy

+0

No pude hacer que el código funcione en Windows Phone, no sé si es un problema de seguridad. Pero no funcionó .. Excepción: {"Error al intentar acceder al método: System.Reflection.Emit.DynamicMethod ..ctor (System.String, Syst em.Type, System.Type [], System.Type) "} –

+1

@ J.Lennon Desafortunadamente, no puedo probarlo en Windows Phone. Así que estaré muy agradecido si pudieras tratar de usar esta [** versión actualizada **] (http://pastebin.com/4za6pdzA) y me harías saber si te ayuda. Gracias por adelantado. –

2

Si usted está dispuesto a tener un método según el tipo de delegado, puede hacer algo como:

Task FromEvent(Action<Action> add) 
{ 
    var tcs = new TaskCompletionSource<bool>(); 

    add(() => tcs.SetResult(true)); 

    return tcs.Task; 
} 

Se podría utilizarlo como:

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

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

Si utiliza delegados genéricos, un método por cada tipo genérico es suficiente, no necesita uno para cada tipo concreto:

Task<T> FromEvent<T>(Action<Action<T>> add) 
{ 
    var tcs = new TaskCompletionSource<T>(); 

    add(x => tcs.SetResult(x)); 

    return tcs.Task; 
} 

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

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

El principal problema es que muchos de los marcos de la interfaz de usuario crean sus propios tipos de delegados para cada evento (en lugar de utilizar 'Acción '/'EventHandler '), y ahí es donde algo como esto sería más útil, creando un El método 'FromEvent' para cada tipo de delegado sería * mejor *, pero aún no perfecto. Dicho esto, podría tener el primer método que creó y usó: 'await FromEvent (x => new MyClass(). OnCompletion + = (a, b) => x());' en cualquier evento. Es una especie de solución a medio camino. – Servy

+0

@Servy Sí, pensé en hacerlo de esa manera también, pero no lo mencioné porque creo que es feo (es decir, demasiado repetitivo). – svick

+0

esta solución es muy fea y difícil de usar = (cuando escribí el código, pensé: wtf !? –

5

Esto le dará lo que necesita sin necesidad de hacer ningún Ilgen, y la manera más sencilla. Funciona con cualquier tipo de delegados 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<object>(); 
    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<T> style) 
static Task<T> TaskFromEvent<T>(object target, string eventName) { 
    var addMethod = target.GetType().GetEvent(eventName).GetAddMethod(); 
    var delegateType = addMethod.GetParameters()[0].ParameterType; 
    var tcs = new TaskCompletionSource<T>(); 
    var resultSetter = (Action<T>)tcs.SetResult; 
    var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke"); 
    addMethod.Invoke(target, new object[] { d }); 
    return tcs.Task; 
} 

// Two-value events (Action<T1, T2> or EventHandler style) 
static Task<Tuple<T1, T2>> TaskFromEvent<T1, T2>(object target, string eventName) { 
    var addMethod = target.GetType().GetEvent(eventName).GetAddMethod(); 
    var delegateType = addMethod.GetParameters()[0].ParameterType; 
    var tcs = new TaskCompletionSource<Tuple<T1, T2>>(); 
    var resultSetter = (Action<T1, T2>)((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<int, string>(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; 
} 

Here's a helper function que va a permitir escribir las funciones TaskFromEvent en una sola línea de cada uno, si los tres métodos anteriores son demasiado copiar y pegar para su preferencias. Se debe dar crédito al máximo para simplificar lo que tenía originalmente.

+0

Thansk mucho !!! Para Windows Phone, esta línea debe ser modificada: var parameters = methodInfo.GetParameters() .Seleccione (a => System.Linq.Expressions.Expression.Parameter (a.ParameterType, a.Name)). ToArray(); –