2012-10-12 28 views
7

使用新的異步/等待模型可以非常簡單地生成在事件觸發時完成的Task;你只需要遵循這個模式:通用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; 
} 

這允許:

await FromEvent(new MyClass()); 

的問題是,你需要爲每一個類每一個事件,你想await創建一個新的FromEvent方法上。這可能真的很快,而且主要是樣板代碼。

我非常希望能夠做這樣的事情:

await FromEvent(new MyClass().OnCompletion); 

然後我可以重新使用相同的FromEvent方法在任何情況下的任何事件。我花了一些時間嘗試創建這樣的方法,並且存在一些障礙。對於它上面的代碼將產生以下錯誤:

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

據我所知,有將永遠不會成爲傳遞活動像這樣通過代碼的方式。

所以,退而求其次似乎想通過事件的字符串名字:

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

它並不像理想;如果該類型的事件不存在,那麼您不會獲得智能感知,並會得到運行時錯誤,但它仍可能比大量的FromEvent方法更有用。

所以很容易使用反射和GetEvent(eventName)來獲得EventInfo對象。接下來的問題是該事件的委託在運行時不知道(並且需要能夠改變)。這使得難以添加事件處理程序,因爲我們需要在運行時動態創建一個方法,匹配給定的簽名(但忽略所有參數),這些簽名訪問我們已有的TaskCompletionSource並設置其結果。

幸運的是,我找到了this link,其中包含有關如何通過Reflection.Emit完成該操作的說明。現在問題是我們需要發射IL,並且我不知道如何訪問我擁有的tcs實例。

下面是我對完成這個方面取得的進展:

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我可不可以發出,讓我來設置TaskCompletionSource的結果呢?或者,另外還有另一種方法來創建一個方法,該方法從任意類型中爲任意任意事件返回一個Task?

+2

請注意,BCL具有'TaskFactory.FromAsync',可以輕鬆地從APM轉換爲TAP。沒有一種簡單的*和*通用的方式可以從EAP轉換爲TAP,所以我認爲這就是爲什麼MS不包括這樣的解決方案。我發現Rx(或TPL Dataflow)與「事件」語義更接近匹配 - 而Rx *也有* FromEvent'方法。 –

+1

我也想做一個通用的'FromEvent <>',並且[this](http://stackoverflow.com/a/22798789/1768303)已經很接近了,因爲我可以在不使用反射的情況下實現這一點。 – Noseratio

回答

21

在這裏你去:

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

此代碼將用於返回void(無論參數列表),幾乎所有事件的工作。

如果需要,可以改進以支持任何返回值。

你可以看到達克斯的和我下面的方法之間的區別:

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

簡單地說,我的代碼支持真的任何委託類型的。您不應(也不需要)明確地指定它,如TaskFromEvent<int, string>

+0

我剛看完你的更新並稍微玩了一下。喜歡它事件處理程序是取消訂閱的,這是一個很好的接觸,各種事件處理程序都被緩存,所以對於相同的類型,IL不會重複生成,並且與其他解決方案不同,不需要指定參數的類型 – Servy

+0

我無法讓代碼在windows phone上工作,不知道是否是安全問題。但是沒有工作。 異常:{「嘗試訪問方法失敗:System.Reflection.Emit.DynamicMethod ..ctor(System.String,Syst em.Type,System.Type [],System.Type)「} –

+1

@ J.Lennon不幸的是,我無法在Windows Phone上測試它。因此,如果您可以嘗試使用此[**更新版本**](http://pastebin.com/4za6pdzA),並且告訴我它是否有幫助,我會非常感激。提前致謝。 –

2

如果你願意讓每個委託類型的一種方法,你可以這樣做:

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

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

    return tcs.Task; 
} 

你會用它喜歡:

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

要知道,這樣你永遠不退訂事件,這可能會或可能不會對您造成問題。

如果您使用泛型委託,按每個泛型類型的方法是不夠的,你並不需要爲每個具體類型:

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

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

    return tcs.Task; 
} 

雖然類型推斷不會與工作,你必須明確地指定類型參數(假設OnCompletion類型是Action<string>這裏):

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

主要的問題是,如此多的UI框架爲每個事件創建自己的委託類型(而不是使用'Action '/'EventHandler '),這就是最有用的東西,所以創建一個每個代表類型的'FromEvent'方法會更好*,但仍不完美。也就是說,您可以只使用第一種方法創建並使用:'在任何事件中等待FromEvent(x => new MyClass()。OnCompletion + =(a,b)=> x());這是一種中途解決方案。 – Servy

+0

@Servy是的,雖然我也是這麼做的,但我沒有提到它,因爲我認爲它很醜陋(即太多的樣板)。 – svick

+0

這個解決方案是非常難看和難用的(當我編寫代碼時,我認爲:wtf!? –

5

這會給你你需要什麼,而無需做任何伊爾根和方式簡單。它適用於任何類型的活動代表;你只需要爲你的事件委託中的每一個參數創建一個不同的處理器。下面是0..2所需的處理程序,它應該是絕大多數用例。擴展到3以上是從2參數方法簡單的複製和粘貼。

這比ilgen方法更強大,因爲您可以使用由異步模式中的事件創建的任何值。

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

用法就是這樣。正如你所看到的,即使事件是在自定義委託中定義的,它仍然有效。並且您可以將這些偶數值捕獲爲一個元組。

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那將讓你寫TaskFromEvent功能在每個只有一條線路,如果上述的三種方法是你的喜好太多的複製和粘貼。爲了簡化我原來的工作,必須給予最大的信貸額度。

+0

Thansk很多!!!對於windows phone,這條線需要修改:var parameters = methodInfo.GetParameters() .Select(a => System.Linq.Expressions.Expression.Parameter(a.ParameterType,a.Name))。ToArray(); –