2012-10-12 1 views
7

En utilisant le nouveau modèle async/await, il est assez simple de générer un Task qui est terminé lorsqu'un événement se déclenche; il vous suffit de suivre ce modèle:Méthode générale 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; 
} 

Ceci permet alors:

await FromEvent(new MyClass()); 

Le problème est que vous devez créer une nouvelle méthode FromEvent pour chaque événement dans toutes les classes que vous souhaitez await sur. Cela pourrait être vraiment très rapide, et c'est surtout du code standard.

Idéalement je voudrais être en mesure de faire quelque chose comme ceci:

await FromEvent(new MyClass().OnCompletion); 

alors je pourrais réutiliser la même méthode FromEvent pour tout événement sur une instance. J'ai passé du temps à essayer de créer une telle méthode, et il y a un certain nombre de problèmes. Pour le code ci-dessus, il va générer l'erreur suivante:

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

Pour autant que je sache, il n'y aura jamais un moyen de passer l'événement comme celui-ci par le code.

Donc, la meilleure chose semblait essayer de passer le nom de l'événement comme une chaîne:

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

Ce n'est pas comme idéal; vous n'obtenez pas intellisense et vous obtiendrez une erreur d'exécution si l'événement n'existe pas pour ce type, mais cela pourrait être plus utile que des tonnes de méthodes FromEvent.

Il est donc assez facile d'utiliser la réflexion et GetEvent(eventName) pour obtenir l'objet EventInfo. Le problème suivant est que le délégué de cet événement n'est pas connu (et doit pouvoir varier) à l'exécution. Cela rend difficile l'ajout d'un gestionnaire d'événements, car nous devons créer dynamiquement une méthode à l'exécution, en faisant correspondre une signature donnée (mais en ignorant tous les paramètres) qui accède à un TaskCompletionSource que nous avons déjà et en définit le résultat.

Heureusement, j'ai trouvé this link qui contient des instructions sur la façon de faire [presque] exactement cela via Reflection.Emit. Maintenant, le problème est que nous devons émettre IL, et je n'ai aucune idée comment accéder à l'instance tcs que j'ai.

est Ci-dessous les progrès que je l'ai fait pour finir ceci:

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

Quelle IL pourrais-je émettre éventuellement qui me permettrait de mettre en résultat de la TaskCompletionSource? Ou, alternativement, y a-t-il une autre approche pour créer une méthode qui renvoie une tâche pour n'importe quel événement arbitraire d'un type arbitraire?

+2

Notez que le BCL a 'TaskFactory.FromAsync' pour traduire facilement APM en TAP. Il n'y a pas de moyen simple et * générique de traduire d'EAP en TAP, donc je pense que c'est pourquoi MS n'a pas inclus une solution comme celle-ci. Je trouve que Rx (ou TPL Dataflow) est de plus en plus proche de la sémantique "event" - et Rx * a une méthode de type FromEvent. –

+1

Je voulais aussi faire un 'FromEvent <>' générique, et [this] (http://stackoverflow.com/a/22798789/1768303) est proche car je pourrais y arriver sans utiliser de réflexion. – Noseratio

Répondre

21

Ici, vous allez:

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

Ce code fonctionnera pour presque tous les événements qui reviennent vides (quelle que soit la liste des paramètres).

Il peut être amélioré pour prendre en charge toutes les valeurs de retour si nécessaire.

Vous pouvez voir la différence entre les méthodes de Dax et le mien ci-dessous:

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 bref, mon code prend en charge vraiment tout type de type délégué. Vous ne devriez pas (et ne devez pas) le spécifier explicitement comme TaskFromEvent<int, string>.

+0

Je viens juste de finir de regarder votre mise à jour et de jouer avec un peu. Le gestionnaire d'événement est désabonné, ce qui est une bonne idée Les différents gestionnaires d'événements sont mis en cache, donc IL n'est pas généré plusieurs fois pour les mêmes types, et, contrairement aux autres solutions, il n'est pas nécessaire de spécifier les types d'arguments – Servy

+0

Je ne pouvais pas faire fonctionner le code sur Windows Phone, je ne sais pas si c'est un problème de sécurité, mais pas travaillé .. Exception: {"Tentative d'accès à la méthode a échoué: System.Reflection.Emit.DynamicMethod ..ctor (System.String, Syst em.Type, System.Type [], System.Type) "} –

+1

@ J.Lennon Malheureusement, je ne suis pas en mesure de le tester sur Windows Phone. Donc je serais vraiment reconnaissant si vous pouviez essayer d'utiliser cette [** version mise à jour **] (http://pastebin.com/4za6pdzA) et laissez-moi savoir si cela aide. Merci d'avance. –

2

Si vous êtes prêt à avoir une méthode selon le type de délégué, vous pouvez faire quelque chose comme:

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

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

    return tcs.Task; 
} 

Vous l'utiliser comme:

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

Soyez conscient que cette façon vous ne se désinscrire de l'événement, cela peut ou peut ne pas être un problème pour vous.

Si vous utilisez des délégués génériques, une méthode pour chaque type générique est suffisant, vous n'avez pas besoin d'un pour chaque type de béton:

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

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

    return tcs.Task; 
} 

Bien que l'inférence de type ne fonctionne pas avec cela, vous doivent spécifier explicitement le paramètre de type (en supposant que le type de OnCompletion est Action<string> ici):

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

Le principal problème est que tant de frameworks UI créent leurs propres types de délégués pour chaque événement (plutôt que d'utiliser 'Action '/'EventHandler '), et c'est là que quelque chose comme ça serait le plus utile, donc créer un La méthode 'FromEvent' pour chaque type de délégué serait * mieux *, mais toujours imparfaite. Cela dit, vous pouvez simplement avoir la première méthode que vous avez faite et utiliser: 'attendre FromEvent (x => new MyClass(). OnCompletion + = (a, b) => x());' sur n'importe quel événement. C'est une solution à mi-chemin. – Servy

+0

@Servy Ouais, je pensais à le faire de cette façon aussi, mais je ne l'ai pas mentionné parce que je pense que c'est moche (c'est-à-dire trop chaud). – svick

+0

cette solution est très laide et difficile à utiliser = (quand j'ai écrit le code je pensais: wtf !? –

5

Cela vous donnera ce dont vous avez besoin sans avoir besoin de faire une Ilgen, et de façon plus simple. Cela fonctionne avec n'importe quel genre de délégués d'événement; Il vous suffit de créer un gestionnaire différent pour chaque nombre de paramètres dans votre délégué d'événement. Voici les gestionnaires dont vous auriez besoin pour 0..2, ce qui devrait être la grande majorité de vos cas d'utilisation. L'extension à 3 et plus est une simple copie et un collage de la méthode à 2 paramètres.

Cette méthode est également plus puissante que la méthode ilgen car vous pouvez utiliser toutes les valeurs créées par l'événement dans votre modèle asynchrone.

// 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'utilisation serait comme ceci. Comme vous pouvez le voir, même si l'événement est défini dans un délégué personnalisé, il fonctionne toujours. Et vous pouvez capturer les valeurs evented comme un tuple.

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 qui vous permet d'écrire les fonctions TaskFromEvent dans une seule ligne chacune, si les trois méthodes ci-dessus sont trop copier-coller pour vos préférences. Le crédit doit être donné à max pour simplifier ce que j'avais à l'origine.

+0

Thansk beaucoup !!! Pour Windows Phone, cette ligne doit être modifiée: var paramètres = methodInfo.GetParameters() .Select (a => System.Linq.Expressions.Expression.Parameter (a.ParameterType, a.Name)). ToArray(); –