Metodo FromEvent per scopi generali

Usando il nuovo modello asincrono / aspetta è abbastanza semplice generare un’attività che viene completata quando un evento si triggers; devi solo seguire questo schema:

 public class MyClass { public event Action OnCompletion; } public static Task FromEvent(MyClass obj) { TaskCompletionSource tcs = new TaskCompletionSource(); obj.OnCompletion += () => { tcs.SetResult(null); }; return tcs.Task; } 

Questo quindi consente:

 await FromEvent(new MyClass()); 

Il problema è che è necessario creare un nuovo metodo FromEvent per ogni evento in ogni class in cui si desidera await . Questo potrebbe diventare davvero grande molto velocemente, ed è per lo più solo codice boilerplate.

Idealmente mi piacerebbe essere in grado di fare qualcosa del genere:

 await FromEvent(new MyClass().OnCompletion); 

Quindi potrei riutilizzare lo stesso metodo FromEvent per qualsiasi evento su qualsiasi istanza. Ho passato un po ‘di tempo a cercare di creare un tale metodo, e ci sono un certo numero di ostacoli. Per il codice qui sopra genererà il seguente errore:

L’evento ‘Namespace.MyClass.OnCompletion’ può apparire solo sul lato sinistro di + = o – =

Per quanto posso dire, non ci sarà mai un modo per passare l’evento in questo modo attraverso il codice.

Quindi, la prossima cosa migliore sembrava provare a passare il nome dell’evento come una stringa:

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

Non è così ideale; non si ottiene intellisense e si otterrebbe un errore di runtime se l’evento non esiste per quel tipo, ma potrebbe essere ancora più utile di tonnellate di metodi FromEvent.

Quindi è abbastanza semplice usare reflection e GetEvent(eventName) per ottenere l’object EventInfo . Il prossimo problema è che il delegato di quell’evento non è noto (e deve essere in grado di variare) in fase di runtime. Ciò rende difficile aggiungere un gestore di eventi, poiché è necessario creare dynamicmente un metodo in fase di runtime, facendo corrispondere una determinata firma (ma ignorando tutti i parametri) che accede a una TaskCompletionSource che abbiamo già e ne imposta il risultato.

Fortunatamente ho trovato questo link che contiene le istruzioni su come fare [quasi] esattamente quello via Reflection.Emit . Ora il problema è che dobbiamo emettere IL, e non ho idea di come accedere all’istanza di tcs che ho.

Di seguito sono riportati i progressi che ho compiuto verso la conclusione di questo:

 public static Task FromEvent(this T obj, string eventName) { var tcs = new TaskCompletionSource(); 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; } 

Che cosa potrei emettere IL che mi consenta di impostare il risultato di TaskCompletionSource ? Oppure, in alternativa, esiste un altro approccio alla creazione di un metodo che restituisce un’attività per qualsiasi evento arbitrario da un tipo arbitrario?

Ecco qui:

 internal class TaskCompletionSourceHolder { private readonly TaskCompletionSource m_tcs; internal object Target { get; set; } internal EventInfo EventInfo { get; set; } internal Delegate Delegate { get; set; } internal TaskCompletionSourceHolder(TaskCompletionSource 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 s_emittedHandlers = new Dictionary(); private static void GetDelegateParameterAndReturnTypes(Type delegateType, out List 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(parameters.Length); for (int i = 0; i < parameters.Length; i++) parameterTypes.Add(parameters[i].ParameterType); returnType = invoke.ReturnType; } public static Task FromEvent(this T obj, string eventName) { var tcs = new TaskCompletionSource(); 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 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 // ie 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 (ie *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 (ie *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; } } 

Questo codice funzionerà per quasi tutti gli eventi che restituiscono void (indipendentemente dall'elenco dei parametri).

Può essere migliorato per supportare eventuali valori di ritorno, se necessario.

Di seguito puoi vedere la differenza tra i metodi di Dax e quelli miei:

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

In breve, il mio codice supporta qualsiasi tipo di delegato. Non dovresti (e non devi) specificarlo esplicitamente come TaskFromEvent .

Questo ti darà ciò di cui hai bisogno senza bisogno di fare il ilgen, e molto più semplice. Funziona con qualsiasi tipo di delegati di eventi; devi solo creare un gestore diverso per ciascun numero di parametri nel tuo evento delegato. Di seguito sono riportati i gestori di cui avresti bisogno per 0..2, che dovrebbe essere la stragrande maggioranza dei tuoi casi d’uso. Estendere a 3 e sopra è una semplice copia e incolla dal metodo a 2 parametri.

Questo è anche più potente del metodo ilgen perché puoi usare qualsiasi valore creato dall’evento nel tuo modello asincrono.

 // 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(); 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 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(); var resultSetter = (Action)tcs.SetResult; var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke"); addMethod.Invoke(target, new object[] { d }); return tcs.Task; } // Two-value events (Action or EventHandler 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>(); var resultSetter = (Action)((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’uso sarebbe come questo. Come puoi vedere, anche se l’evento è definito in un delegato personalizzato, funziona comunque. E puoi catturare i valori degli eventi come una tupla.

 static async void Run() { var result = await TaskFromEvent(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; } 

Ecco una funzione di supporto che ti consente di scrivere le funzioni TaskFromEvent in una sola riga ciascuna, se i tre metodi precedenti sono troppo copia e incolla per le tue preferenze. Il credito deve essere dato al massimo per semplificare ciò che avevo originariamente.

Se sei disposto ad avere un metodo per tipo di delegato, puoi fare qualcosa come:

 Task FromEvent(Action add) { var tcs = new TaskCompletionSource(); add(() => tcs.SetResult(true)); return tcs.Task; } 

Lo useresti come:

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

Tieni presente che in questo modo non annulli mai l’iscrizione all’evento, che potrebbe essere o meno un problema per te.

Se si utilizzano delegati generici, è sufficiente un metodo per ciascun tipo generico, non è necessario uno per ogni tipo concreto:

 Task FromEvent(Action> add) { var tcs = new TaskCompletionSource(); add(x => tcs.SetResult(x)); return tcs.Task; } 

Sebbene l’inferenza di tipo non funzioni con questo, devi specificare esplicitamente il parametro type (assumendo che il tipo di OnCompletion sia Action qui):

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