2012-10-12 23 views
7

Yeni async/await modelini kullanarak bir olay tetiklendiğinde tamamlanan bir Task oluşturmak oldukça kolaydır;Genel amaç FromEvent yöntemi

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

Bu da verir:

await FromEvent(new MyClass()); 

sorunu da await istiyorum her sınıfta her olay için yeni bir FromEvent yöntem oluşturmak için gerektiğidir sadece bu modeli takip etmek gerekir üzerinde. Bu gerçekten çok hızlı bir şekilde gerçekten çok hızlı olabilir ve çoğunlukla sadece bir kod yazısıdır.

İdeal böyle bir şey yapabilmek istiyorum:

await FromEvent(new MyClass().OnCompletion); 

Sonra herhangi bir örneği herhangi bir olay için aynı FromEvent yöntemi yeniden kullanabilirsiniz. Böyle bir yöntem oluşturmaya çalışırken biraz zaman harcadım ve bir kaç engel var. üstündeki kod aşağıdaki hata üretir için: Bildiğim kadarıyla söyleyebilirim

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

, şimdiye kadar kod aracılığıyla böyle olayı geçen bir yolu var olmayacak.

Yani, sonraki en iyi şey bir dize olarak etkinlik adını geçiş yapmaya çalışan gibiydi: O kadar ideal değil

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

; intellisense alamazsınız ve olay bu tür için mevcut değilse bir çalışma zamanı hatası alırsınız, ancak yine de çok sayıda FromEvent yönteminden daha yararlı olabilir.

nesnesini almak için yansıma ve GetEvent(eventName) kullanmanız yeterli. Bir sonraki problem, o olayın temsilcisinin çalışma zamanında bilinmemesi (ve değişebilmesi gerekmesi). Bu, bir olay işleyicisinin eklenmesi zorlaştırır çünkü çalışma zamanında bir yöntem oluşturmak, zaten sahip olduğumuz ve sonucunu belirleyen bir TaskCompletionSource erişen belirli bir imzayı (ancak tüm parametreleri göz ardı ederek) eşleştirmemiz gerekir.

Neyse ki this link'u buldum ve bu sayede [neredeyse] tam olarak Reflection.Emit aracılığıyla nasıl yapılacağına dair yönergeleri içerir. Şimdi sorun, IL yayamamamız ve benim sahip olduğum tcs örneğine nasıl erişeceğimize dair bir fikrim yok.

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

IL muhtemelen bu beni TaskCompletionSource sonucunu ayarlamak için izin verecek yayarlar ne olabilir: Aşağıda

Ben bu bitirme yolunda kaydettikleri ilerlemeyi nedir? Veya alternatif olarak, keyfi bir türden herhangi bir keyfi olay için bir Görev döndüren bir yöntem oluşturmaya yönelik başka bir yaklaşım var mı? Eğer temsilci türü başına bir yöntemine sahip istekli iseniz

+2

BCL'nin 'TaskFactory.FromAsync' adlı dosyayı APM'den TAP'a kolayca çevirebileceğini unutmayın. EAP'tan TAP'a çevirmenin kolay ve genel bir yolu yoktur, bu yüzden MS'nin böyle bir çözüm içermediğini düşünüyorum. Rx (veya TPL Dataflow) 'nu "event" semantiği ile daha yakın bir eşleşme olarak buluyorum ve Rx * 'nin bir' FromEvent' yöntemi vardır. –

+1

Ayrıca, bir "FromEvent <>" jenerik yapmak istedim ve [this] (http://stackoverflow.com/a/22798789/1768303), yansımayı kullanmadan ona ulaşabileceğim kadar yakın. – Noseratio

cevap

21

gitmek:

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

Bu kod void (bakılmaksızın parametre listesinin) dönmek neredeyse tüm etkinlikler için çalışacaktır.

Gerekirse herhangi bir dönüş değerini desteklemek için geliştirilebilir.

Sen Dax ile benim yöntemlerinin altındaki arasındaki farkı görebilirsiniz:

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

Kısaca, benim kod gerçekten temsilci türünün her türlü destekler. Açıkça TaskFromEvent<int, string> gibi belirtmemelisiniz (ve buna gerek yoktur).

+0

Güncellemeye göz atmayı ve biraz oynatmayı bitirdim. Etkinlik işleyicisi abonelikten kaldırıldı, bu da müthiş bir dokunuş.Çeşitli olay işleyicileri önbelleğe alınır, bu yüzden IL aynı tipler için tekrar tekrar üretilmez ve diğer çözümlerin aksine argüman türlerini belirtmeye gerek yoktur. Olay işleyicisine – Servy

+0

Kod Windows Phone üzerinde çalışamam, bir güvenlik sorunu olup olmadığını bilmiyorum.Ama işe yaramadı .. Özel durum: {"Bu yönteme erişme girişimi başarısız oldu: System.Reflection.Emit.DynamicMethod ..ctor (Sistem.String, Sist em.Type, System.Type [], System.Type) "} –

+1

@ J.Lennon Ne yazık ki, Windows Phone üzerinde test edemiyorum. Bu [** güncelleştirilmiş sürümü **] (http://pastebin.com/4za6pdzA) kullanmayı deneyebilir ve yardım ederse bana bildirirseniz çok minnettar olacağım. Şimdiden teşekkürler. –

2

, gibi bir şey yapabilirsiniz:

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

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

    return tcs.Task; 
} 

Sen gibi kullanırsınız:

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

farkında olun ki bu şekilde asla Etkinlikten çıkmak, sizin için bir sorun olabilir veya olmayabilir.tür kesmesi, bu konuda size çalışmıyor

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

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

    return tcs.Task; 
} 

rağmen:

Eğer jenerik delege kullanıyorsanız, her genel türü başına bir yöntem, her beton türü için, gerek de yok, yeter açıkça türü parametresi belirtmek zorunda (OnCompletion tipini varsayarak burada Action<string> olan):

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

Buradaki ana sorun, UI çerçevelerinin çoğunun her olay için kendi delege türlerini oluşturmasıdır ('Action '/' EventHandler 'yerine) ve işte bu gibi bir şey en yararlı olanıdır. Her bir temsilci türü için 'FromEvent 'yöntemi * daha iyi * olabilir, ancak yine de mükemmel değildir. Bu, yaptığınız ve kullandığınız ilk yönteme sahip olabileceğinizi söyledi: 'herhangi bir olayda FromEvent (x => yeni MyClass(). OnCompletion + = (a, b) => x()); Bu bir çeşit yarım yollu çözüm. – Servy

+0

@Servy Evet, ben de bu şekilde yapmaktan bahsediyorum, ama bahsetmedim çünkü çirkin olduğunu düşünüyorum (yani çok fazla boilerplate). – svick

+0

Bu çözüm çok çirkin ve kullanımı zor = (Ben düşündüğüm kodu yazdığımda: wtf! –

5

Bu, herhangi bir İlgen ve yolu daha basit yapmanıza gerek kalmadan ihtiyacın olanı verecektir. Her türlü olay delegesi ile çalışır; Sadece olay temsilcisinizdeki her bir parametre için farklı bir işleyici oluşturmanız gerekir. Aşağıda, kullanım durumlarınızın büyük çoğunluğu olması gereken 0..2 için gereken işleyicileri bulabilirsiniz. 3'e ve daha fazlasına genişletmek, 2 parametreli yöntemden basit bir kopyalama ve yapıştırmadır.

Bu, aynı zamanda ilgen yönteminden daha güçlüdür çünkü olay tarafından oluşturulan herhangi bir değeri, eşzamansız düzeninizde kullanabilirsiniz.

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

Kullanım böyle olurdu. Gördüğünüz gibi, olay özel bir delege içinde tanımlanmış olsa da, hala çalışır. Ve olayları bir tuple olarak yakalayabilirsiniz. Yukarıdaki üç yöntem tercihlerinize için çok fazla kopyalama ve yapıştırma ise, sadece bir satır her TaskFromEvent fonksiyonlarını yazmak için izin verirsiniz

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. Aslen sahip olduğum şeyi basitleştirmek için azami kredi verilmelidir. İşte

+0

Thansk çok! Windows telefon için, bu satırın değiştirilmesi gerekiyor: var parameters = methodInfo.GetParameters() .Select (a => System.Linq.Expressions.Expression.Parameter (a.ParameterType, a.Name)) ToArray(); –

İlgili konular