2012-10-12 3 views
7

Mit dem neuen async/await-Modell ist es relativ einfach, eine Task zu generieren, die abgeschlossen wird, wenn ein Ereignis ausgelöst wird. Sie müssen nur dieses Muster folgen:Universelle FromEvent-Methode

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

Dies erlaubt dann:

await FromEvent(new MyClass()); 

Das Problem ist, dass Sie eine neue FromEvent Methode für jedes Ereignis in jeder Klasse erstellen, die Sie möchten await auf. Das könnte sehr schnell sehr groß werden, und es ist sowieso nur ein Vorabcode.

Im Idealfall würde Ich mag Lage sein, so etwas zu tun:

await FromEvent(new MyClass().OnCompletion); 

Dann könnte ich die gleiche FromEvent Methode für jeden Fall auf jeden Fall wiederverwenden. Ich habe einige Zeit damit verbracht, eine solche Methode zu erstellen, und es gibt eine Reihe von Hindernissen. Für den obigen Code den folgenden Fehler erzeugen:

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

Soweit ich das beurteilen kann, wird es nicht immer eine Möglichkeit, auf das Bestehen der Veranstaltung wie diese durch Code sein.

So schien die nächste beste Sache, zu versuchen, die Ereignisnamen als String zu übergeben:

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

Es ist nicht so ideal; Sie erhalten kein intellisense und erhalten einen Laufzeitfehler, wenn das Ereignis für diesen Typ nicht existiert, aber es könnte immer noch nützlicher sein als Tonnen von FromEvent-Methoden.

So ist es einfach genug, Reflexion und GetEvent(eventName) zu verwenden, um das EventInfo Objekt zu erhalten. Das nächste Problem ist, dass der Delegat dieses Ereignisses zur Laufzeit nicht bekannt ist (und in der Lage sein muss, zu variieren). Das erschwert das Hinzufügen eines Ereignishandlers, da wir zur Laufzeit dynamisch eine Methode erstellen müssen, die eine bestimmte Signatur (die alle Parameter ignoriert) abgleicht, die auf eine TaskCompletionSource zugreift, die wir bereits haben, und ihr Ergebnis setzt.

Glücklicherweise fand ich this link, die Anweisungen enthält, wie [fast] genau das über Reflection.Emit zu tun. Jetzt ist das Problem, dass wir IL ausstrahlen müssen, und ich habe keine Ahnung, wie ich auf die tcs Instanz zugreifen kann, die ich habe.

Unten finden Sie die Fortschritte, die ich diese in Richtung Fertigstellung gemacht haben:

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

Was IL könnte möglicherweise emittieren ich, dass mir erlauben würde, das Ergebnis der TaskCompletionSource zu setzen? Oder gibt es alternativ einen anderen Ansatz zum Erstellen einer Methode, die einen Task für ein beliebiges Ereignis von einem beliebigen Typ zurückgibt?

+2

Beachten Sie, dass die BCL über 'TaskFactory.FromAsync' verfügt, um einfach von APM nach TAP zu übersetzen. Es gibt keine einfache * und * generische Möglichkeit, von EAP zu TAP zu übersetzen, deshalb denke ich, dass MS deshalb keine Lösung wie diese enthielt. Ich finde, dass Rx (oder TPL Dataflow) sowieso einer "event" -Semantik näher kommt - und Rx * hat eine 'FromEvent'-Methode. –

+1

Ich wollte auch ein generisches 'FromEvent <>' machen, und [dies] (http://Stackoverflow.com/a/22798789/1768303) ist nah dran, wie ich das ohne Reflektion erreichen konnte. – Noseratio

Antwort

21

Hier gehen Sie:

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

Dieser Code wird für fast alle Veranstaltungen arbeiten, die void zurückgeben (unabhängig von der Parameterliste).

Es kann verbessert werden, um etwaige Rückgabewerte bei Bedarf zu unterstützen.

Sie können den Unterschied zwischen Dax und mir Methoden unten:

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

Kurz gesagt, unterstützt meinen Code wirklich jede Art von Delegattyp. Sie sollten (und müssen nicht) explizit wie TaskFromEvent<int, string> angeben.

+0

Ich bin gerade fertig mit der Suche nach Ihrem Update und spiele damit ein bisschen herum Der Event-Handler ist abgemeldet, was eine große Sache ist: Die verschiedenen Event-Handler werden zwischengespeichert, so dass IL nicht wiederholt für die gleichen Typen generiert wird, und im Gegensatz zu anderen Lösungen keine Notwendigkeit besteht, die Arten von Argumenten anzugeben zum Ereignishandler – Servy

+0

Ich konnte den Code nicht auf Windows-Telefon funktionieren lassen, weiß nicht, ob es ein Sicherheitsproblem ist, aber nicht gearbeitet .. Ausnahme: {"Versuch, auf die Methode zuzugreifen fehlgeschlagen: System.Reflection.Emit.DynamicMethod ..ctor (System.String, Syst em.Type, System.Type [], System.Type) "} –

+1

@J.Lennon Leider kann ich es nicht auf dem Windows Phone testen. Also ich werde wirklich dankbar sein, wenn Sie versuchen könnten, diese [** aktualisierte Version **] (http://pastebin.com/4za6pdzA) zu verwenden und lassen Sie mich wissen, wenn es hilft. Danke im Voraus. –

2

Wenn Sie bereit sind, eine Methode pro Delegattyp haben, können Sie wie etwas tun:

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

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

    return tcs.Task; 
} 

möchten Sie es verwenden:

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

Beachten Sie, dass diese Art und Weise Sie nie Abmelden von der Veranstaltung, die für Sie ein Problem darstellen kann oder auch nicht.

Wenn Sie generische Delegaten verwenden, ein Verfahren für jeden generischen Typen genug ist, brauchen Sie nicht eine für jeden konkreten Typen:

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

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

    return tcs.Task; 
} 

Obwohl Typinferenz mit, dass nicht funktioniert, Sie explizit den Typ-Parameter angeben müssen (die Art der OnCompletion unter der Annahme Action<string> hier):

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

Das Hauptproblem dort ist, dass so viele der UI-Frameworks ihre eigenen Delegate-Typen für jedes Ereignis erstellen (statt "Aktion '/'EventHandler '), und das ist, wo so etwas am nützlichsten wäre, also ein 'FromEvent'-Methode für jeden Delegattyp wäre * besser *, aber immer noch nicht perfekt. Das heißt, Sie könnten nur die erste Methode haben, die Sie gemacht haben und verwenden: 'erwarten FromEvent (x => neue MyClass(). OnCompletion + = (a, b) => x());' bei jedem Ereignis. Es ist eine Art halbwegs Lösung. – Servy

+0

@Servy Ja, ich habe es auch so gemacht, aber ich habe es nicht erwähnt, weil ich denke, es ist hässlich (d. H. Zu viel Standard). – svick

+0

Diese Lösung ist sehr hässlich und schwer zu benutzen = (als ich den Code schrieb dachte ich: wtf !? –

5

das werden Sie, was Sie brauchen, ohne dass ilgen zu tun brauchen, und Art und Weise einfacher. Es funktioniert mit jeder Art von Event-Delegierten; Sie müssen nur einen anderen Handler für jede Anzahl von Parametern in Ihrem Ereignisdelegaten erstellen. Unten finden Sie die Handler, die Sie für 0..2 benötigen. Dies sollte die überwiegende Mehrheit Ihrer Anwendungsfälle sein. Die Erweiterung auf 3 und höher ist ein einfaches Kopieren und Einfügen aus der 2-Parameter-Methode.

Dies ist auch leistungsfähiger als die ILGEN-Methode, da Sie alle von dem Ereignis in Ihrem asynchronen Muster erstellten Werte verwenden können.

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

Verwendung wäre so. Wie Sie sehen, funktioniert das Ereignis, obwohl es in einem benutzerdefinierten Delegaten definiert ist. Und Sie können die gerundeten Werte als Tupel erfassen.

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, dass Sie die TaskFromEvent Funktionen schreiben lassen würde in einer Zeile nur jeweils, wenn die drei oben genannten Methoden zu viel copy-and-paste sind für Ihre Vorlieben. Kredit muss an max gegeben werden, um das zu vereinfachen, was ich ursprünglich hatte.

+0

Thansk viel !!! Für Windows Phone muss diese Zeile geändert werden: var parameters = methodInfo.GetParameters() .Wählen Sie (a => System.Linq.Expressions.Expression.Parameter (a.ParameterType, a.Name)). ToArray(); –