2012-10-12 20 views
7

Utilizzando il nuovo modello async/await è abbastanza semplice generare un Task che viene completato quando si verifica un evento; non vi resta che seguire questo modello:Uso generico Metodo 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; 
} 

Questo permette quindi:

await FromEvent(new MyClass()); 

Il problema è che è necessario creare un nuovo metodo FromEvent per ogni evento in ogni classe che si desidera await sopra. Questo potrebbe diventare davvero grande molto velocemente, ed è per lo più solo codice boilerplate.

Idealmente mi piacerebbe essere in grado di fare qualcosa del genere:

await FromEvent(new MyClass().OnCompletion); 

Poi ho potuto ri-utilizzare lo stesso metodo FromEvent per qualsiasi evento in qualsiasi istanza. Ho passato un po 'di tempo a cercare di creare un tale metodo, e ci sono un certo numero di ostacoli. Per il codice di cui sopra verrà generato il seguente errore:

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

Per quanto posso dire, non ci sarà mai un modo di passare l'evento come questo tramite il codice.

Quindi, la cosa migliore sembrava essere cercando di passare il nome dell'evento come una stringa:

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

Non è come l'ideale; non si ottiene intellisense e si otterrebbe un errore di runtime se l'evento non esiste per quel tipo, ma potrebbe essere ancora più utile di tonnellate di metodi FromEvent.

Quindi è abbastanza facile usare la riflessione e GetEvent(eventName) per ottenere l'oggetto EventInfo. Il prossimo problema è che il delegato di quell'evento non è noto (e deve essere in grado di variare) in fase di runtime. Questo rende difficile aggiungere un gestore di eventi, perché abbiamo bisogno di creare dinamicamente un metodo in fase di runtime, facendo corrispondere una determinata firma (ma ignorando tutti i parametri) che accede a uno TaskCompletionSource che abbiamo già e ne imposta il risultato.

Fortunatamente ho trovato this link che contiene istruzioni su come eseguire [quasi] esattamente quello tramite Reflection.Emit. Ora il problema è che abbiamo bisogno di emettere IL, e non ho idea di come accedere all'istanza tcs che ho.

Di seguito i progressi che ho fatto nei confronti di finitura questo:

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; 
} 

Quello IL potrei emettere che mi avrebbe permesso di impostare il risultato della TaskCompletionSource? Oppure, in alternativa, esiste un altro approccio alla creazione di un metodo che restituisce un'attività per qualsiasi evento arbitrario da un tipo arbitrario?

+2

Si noti che il BCL ha 'TaskFactory.FromAsync' per tradurre facilmente da APM a TAP. Non esiste un modo semplice * e * generico per tradurre da EAP a TAP, quindi penso che sia per questo che MS non ha incluso una soluzione come questa. Trovo che Rx (o TPL Dataflow) sia più vicino alla semantica "evento" - e Rx * ha * un metodo di tipo 'FromEvent'. –

+1

Volevo anche creare un generico 'FromEvent <>' e [this] (http://stackoverflow.com/a/22798789/1768303) è vicino a quello che potrei ottenere senza usare il reflection. – Noseratio

risposta

21

Qui si va:

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; 
    } 
} 

Questo codice funziona per quasi tutti gli eventi che restituiscono void (a prescindere dalla lista dei parametri).

Può essere migliorato per supportare eventuali valori di ritorno, se necessario.

si può vedere la differenza tra Dax e di metodi miniera di seguito:

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; 
} 

In breve, il mio codice supporta davvero qualsiasi tipo di tipo delegato. Non dovresti (e non devi) specificarlo esplicitamente come TaskFromEvent<int, string>.

+0

Ho appena finito di esaminare il tuo aggiornamento e di giocarci un po '. Il gestore di eventi è annullato, il che è un bel tocco.I vari gestori di eventi sono memorizzati nella cache, quindi IL non viene generato ripetutamente per gli stessi tipi e, a differenza di altre soluzioni, non è necessario specificare i tipi di argomenti al gestore di eventi – Servy

+0

Non è possibile eseguire il codice su Windows Phone, non so se si tratta di un problema di sicurezza, ma non ha funzionato .. Eccezione: {"Tentativo di accedere al metodo non riuscito: System.Reflection.Emit.DynamicMethod ..ctor (System.String, Syst em.Type, System.Type [], System.Type) "} –

+1

@ J.Lennon Sfortunatamente, non sono in grado di testarlo su Windows Phone. Quindi sarò davvero grato se potessi provare a usare questa [** versione aggiornata **] (http://pastebin.com/4za6pdzA) e fammi sapere se è d'aiuto. Grazie in anticipo. –

2

Se siete disposti ad avere un metodo per ogni tipo di delegato, si può fare qualcosa di simile:

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

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

    return tcs.Task; 
} 

si dovrebbe utilizzare come:

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

essere consapevoli del fatto che in questo modo non hai mai annullare l'iscrizione all'evento, che potrebbe essere o meno un problema per te.

Se stai usando i delegati generici, un metodo per ogni tipo generico è sufficiente, non hai bisogno di uno per ogni tipo concreto:

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

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

    return tcs.Task; 
} 

Sebbene inferenza di tipo non funziona con questo, necessario specificare in modo esplicito il parametro di tipo (assumendo che il tipo di OnCompletion è Action<string> qui):

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

Il problema principale è che molti dei framework UI creano i propri tipi di delegati per ciascun evento (piuttosto che usare l'azione '/' EventHandler '), ed è qui che qualcosa di simile sarebbe molto utile, quindi creare un Il metodo 'FromEvent' per ciascun tipo di delegato sarebbe * migliore *, ma non perfetto. Detto questo, puoi semplicemente avere il primo metodo che hai creato e utilizzato: 'aspetta FromEvent (x => new MyClass(). OnCompletion + = (a, b) => x());' su qualsiasi evento. È una soluzione a metà strada. – Servy

+0

@Servy Sì, ho pensato di farlo anche in questo modo, ma non l'ho menzionato perché penso che sia brutto (cioè troppo piatto). – svick

+0

questa soluzione è molto brutta e difficile da usare = (quando ho scritto il codice ho pensato: wtf !? –

5

Questo vi darà quello che ti serve senza bisogno di fare qualsiasi ilgen, e il modo più semplice. Funziona con qualsiasi tipo di delegati di eventi; devi solo creare un gestore diverso per ciascun numero di parametri nel tuo evento delegato. Di seguito sono riportati i gestori di cui avresti bisogno per 0..2, che dovrebbe essere la stragrande maggioranza dei tuoi casi d'uso. Estendere a 3 e sopra è una semplice copia e incolla dal metodo a 2 parametri.

Questo è anche più potente del metodo ilgen perché è possibile utilizzare qualsiasi valore creato dall'evento nel proprio modello asincrono.

// 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; 
} 

L'uso sarebbe come questo. Come puoi vedere, anche se l'evento è definito in un delegato personalizzato, funziona comunque. E puoi catturare i valori degli eventi come 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 che ti permette di scrivere le funzioni TaskFromEvent in una sola linea di ognuno, se questi tre metodi sono troppi copia-e-incolla per le tue preferenze. Il credito deve essere dato al massimo per semplificare ciò che avevo originariamente.

+0

Grazie ancora !!! Per windows phone, questa riga deve essere modificata: var parameters = methodInfo.GetParameters() .Seleziona (a => System.Linq.Expressions.Expression.Parameter (a.ParameterType, a.Name)). ToArray(); –