2012-10-12 13 views
7

Dzięki nowemu modelowi async/await można wygenerować Task, która jest wykonywana po uruchomieniu zdarzenia; po prostu trzeba się do tego wzoru:Ogólna metoda 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; 
} 

Pozwala to następnie:

await FromEvent(new MyClass()); 

Problem polega na tym, że trzeba stworzyć nową metodę FromEvent dla każdego zdarzenia w każdej klasie, które chcieliby Państwo await na. To może stać się naprawdę duże naprawdę szybkie, a to w większości przypadków tylko kodowany kod.

Idealnie chciałbym być w stanie zrobić coś takiego:

await FromEvent(new MyClass().OnCompletion); 

Potem może ponownie użyć tej samej metody FromEvent dla każdego zdarzenia w każdym przypadku. Spędziłem trochę czasu, próbując stworzyć taką metodę, i istnieje wiele przeszkód. Na powyższy kod to wygeneruje następujący błąd:

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

O ile mogę powiedzieć, że nigdy nie będzie to sposób przekazywania zdarzenie jak to poprzez kod.

Tak więc, następnym najlepszą rzeczą wydawało się być stara się przekazać nazwę zdarzenia jako wyrażenie:

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

To nie jest tak idealny; nie dostaniesz intellisense i otrzymasz błąd runtime, jeśli zdarzenie nie istnieje dla tego typu, ale może być bardziej przydatne niż tony metod FromEvent.

Łatwo jest użyć odbicia i GetEvent(eventName), aby uzyskać obiekt EventInfo. Kolejnym problemem jest to, że delegat tego wydarzenia nie jest znany (i musi być w stanie zmieniać) w czasie wykonywania. To sprawia, że ​​dodanie obsługi zdarzenia jest trudne, ponieważ musimy dynamicznie tworzyć metodę w czasie wykonywania, dopasowując dany podpis (ale ignorując wszystkie parametry), który uzyskuje dostęp do TaskCompletionSource, który już mamy i ustawia jego wynik.

Na szczęście znalazłem this link, który zawiera instrukcje jak wykonać [prawie] dokładnie to poprzez Reflection.Emit. Problem polega na tym, że musimy emitować IL i nie mam pojęcia, jak uzyskać dostęp do instancji tcs, którą mam.

Poniżej jest postęp, jaki zrobiłem w kierunku kończąc w ten sposób:

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

Co IL mógłbym emitować że pozwoli mi ustawić wynik TaskCompletionSource? Lub alternatywnie, czy istnieje inne podejście do tworzenia metody zwracającej zadanie dla dowolnego zdarzenia z dowolnego typu?

+2

Należy zauważyć, że BCL ma 'TaskFactory.FromAsync', aby łatwo przetłumaczyć z APM na TAP. Nie ma łatwego * i * ogólnego sposobu tłumaczenia z EAP na TAP, więc myślę, że właśnie dlatego MS nie zawierało takiego rozwiązania. Uważam, że Rx (lub TPL Dataflow) jest bliższy semantyce "eventowej" - a Rx * ma * rodzaj metody "FromEvent". –

+1

Chciałem również zrobić ogólne 'FromEvent <>', i [this] (http://stackoverflow.com/a/22798789/1768303) jest blisko, jak mogłem dostać się do tego bez użycia refleksji. – Noseratio

Odpowiedz

21

Proszę bardzo:

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

Ten kod będzie pracować dla niemal wszystkich wydarzeń, które zwracają void (niezależnie od listy parametrów).

Można poprawić, aby w razie potrzeby obsługiwać wszelkie wartości zwracane.

Widać różnicę między Dax i metod górniczych poniżej:

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

Krótko, mojego kodu obsługuje naprawdę wszelkiego rodzaju typ delegata. Nie powinieneś (i nie musisz) określać go jawnie, tak jak TaskFromEvent<int, string>.

+0

Właśnie skończyłem przeglądać twoją aktualizację i trochę się z nią bawię. Program obsługi zdarzeń jest anulowany, co jest świetnym akcentem.Różne funkcje obsługi zdarzeń są buforowane, więc IL nie jest generowane wielokrotnie dla tego samego typu iw przeciwieństwie do innych rozwiązań, nie ma potrzeby określania typów argumentów – Servy

+0

Nie mogłem sprawić, aby kod działał na telefonie z Windows, nie wiem, czy jest to problem bezpieczeństwa, ale nie działa. Wyjątek: {"Próba dostępu do metody nie powiodła się: System.Reflection.Emit.DynamicMethod ..ctor (System.String, Syst em.Type, System.Type [], System.Type) "} –

+1

@ J.Lennon Niestety, nie jestem w stanie przetestować tego na Windows Phone. Będę więc wdzięczny, jeśli spróbujesz użyć tej [** zaktualizowanej wersji **] (http://pastebin.com/4za6pdzA) i daj mi znać, jeśli to pomoże. Z góry dziękuję. –

2

Jeśli jesteś gotów mieć jedną metodę na jeden typ delegata, można zrobić coś takiego:

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

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

    return tcs.Task; 
} 

byłoby go używać jak:

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

Należy pamiętać, że w ten sposób nigdy zrezygnuj z subskrypcji, co może, ale nie musi być dla ciebie problemem.

Jeśli używasz delegatów rodzajowe, jedna metoda dla każdego typu rodzajowego wystarczy, nie trzeba jeden dla każdego rodzaju betonu:

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

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

    return tcs.Task; 
} 

Chociaż rodzaj wnioskowania nie działa z tym, ty trzeba wyraźnie określić parametr typu (zakładając rodzaj OnCompletion jest Action<string> tutaj):

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

Głównym problemem jest to, że tak wiele frameworków UI tworzy własne typy delegatów dla każdego zdarzenia (zamiast używać 'Action '/'EventHandler ') i właśnie tam coś takiego byłoby najbardziej przydatne, więc tworzenie Metoda 'FromEvent' dla każdego typu delegata byłaby * lepsza *, ale nadal nie jest doskonała. Powiedziałeś, że możesz mieć pierwszą metodę, którą stworzyłeś i której używasz: 'await FromEvent (x => new MyClass(). OnCompletion + = (a, b) => x());' przy każdym zdarzeniu. Jest to rozwiązanie w połowie drogi. – Servy

+0

@Servy Tak, chciałem też zrobić to w ten sposób, ale nie wspomniałem o tym, ponieważ uważam, że jest brzydki (to znaczy za dużo wzorca). – svick

+0

to rozwiązanie jest bardzo brzydkie i trudne w użyciu = (gdy napisałem kod myślałem: wtf !? –

5

to da ci to, czego potrzebujesz, bez konieczności robić żadnych Ilgen oraz sposób prostszy. Działa z każdym rodzajem delegatów wydarzeń; po prostu musisz utworzyć inną procedurę obsługi dla każdej liczby parametrów w delegacie wydarzenia. Poniżej znajdują się programy do obsługi potrzebne w wersji 0..2, które powinny stanowić zdecydowaną większość przypadków użycia. Rozszerzanie do 3 i więcej to prosta kopia i wklejanie z metody 2-parametrowej.

Jest to również potężniejsze niż metoda ilgen, ponieważ możesz użyć dowolnych wartości utworzonych przez zdarzenie w swoim asynchronicznym wzorze.

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

Użyj byłoby tak. Jak widać, nawet jeśli zdarzenie jest zdefiniowane w niestandardowym uczestniku, nadal działa. I można uchwycić wartości zdarzeń jako krotkę.

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 że będziemy pozwalają na zapis funkcji TaskFromEvent w jednym wierszu każdego, jeśli powyższe trzy metody są zbyt dużo kopiowania i wklejania do swoich preferencji. Kredyt należy przyznać maks. Za uproszczenie tego, co miałem pierwotnie.

+0

Thansk dużo !!! Dla Windows Phone, linia ta musi zostać zmodyfikowana: var parameters = methodInfo.GetParameters() . Wybierz (a => System.Linq.Expressions.Expression.Parameter (a.ParameterType, a.Name)) ToArray(); –