2015-04-15 6 views
7

sembra come dovrei avere tutto ciò di cui ho bisogno qui, ma i dettagli di farlo accadere mi stanno facendo impazzire.Come si può usare un lambda per creare un nuovo EventHandler?

Ho un metodo di utilità statico che accetta un oggetto client di servizio Web, estrae un EventInfo designato da esso e dovrebbe aggiungere alcuni gestori a quell'evento, essenzialmente i callback per il completamento del richiamo del servizio web. Il problema è che, dato che ogni dato evento ha essenzialmente un proprio sovraccarico di gestore (il codice generato da WCF fornisce un SomeMethodCompletedEventArgs unico per ogni metodo e evento corrispondente), non riesco a capire come farlo accadere.

Ho due gestori che voglio attaccare, e la prima è solo una funzione lambda:

(obj, args) => task.Complete() 

Quindi quello che vorrei fare è proprio questo semplice:

eventInfo.AddEventHandler(client, new EventHandler((obj, args) => task.Complete())); 

Ciò, tuttavia, genera un InvalidCastException di runtime, poiché eventInfo si aspetta un EventHandler<SomeMethodCompletedEventArgs>, non un semplice EventHandler. Credo che ciò significhi che ho bisogno di creare dinamicamente il delegato EventHandler usando lo eventInfo.EventHandlerType in qualche modo, ma non ho capito di combinarlo con la funzione lambda, o altrimenti fare un ricevitore che davvero non lo fa care quale particolare sapore di EventArgs viene Usato.

L'unica soluzione alternativa che ho trovato è creare un argomento modello generico con cui passare il tipo di argomento evento specifico. Questo mi permette di andare:

eventInfo.AddEventHandler(client, new EventHandler<E>(...)); 

Dove E è il parametro in questione. Questo è ovviamente clunky, e sembra proprio sbagliato dover passare questo quando l'estratto eventInfo dovrebbe dirci tutto ciò che dobbiamo sapere.

Vale la pena notare che sto utilizzando un PCL Framework leggermente vincolato per Xamarin, che a quanto pare non include il metodo statico Delegate.CreateDelegate() che ho visto menzionato in problemi correlati. I do hanno accesso a Activator, tuttavia, che dovrebbe coprire la maggior parte delle stesse basi.

+0

Si noti che è generalmente sconsigliato per fissare lambda a gestori di eventi, poiché possono essere rimossi solo da un processo di riflessione piuttosto complicato. –

+1

@ObliviousSage: _ "possono essere rimossi solo da un processo di riflessione piuttosto complicato" _ - non è corretto. Innanzitutto, in molti casi non è mai necessario rimuovere il gestore eventi. In secondo luogo, negli scenari in cui è necessario, tutto ciò che è necessario è conservare il riferimento dell'istanza delegata in una variabile da qualche parte e utilizzare tale valore per annullare l'iscrizione al gestore. Non è necessaria alcuna riflessione e in molti casi questa variabile può essere semplicemente una variabile locale catturata. –

+0

@PeterDuniho Sì, i gestori di eventi spesso non devono essere rimossi; non è come dire che dovresti assumere che non sarà necessario rimuoverlo. Sei corretto che un delegato possa essere rimosso se hai salvato un riferimento ad esso, ma un lambda puro (ad esempio, 'myObject.SomeEvent + = (a, b) => Foo();') non può essere rimosso senza la riflessione precedentemente citata. Che gli svantaggi dell'uso di una lambda come gestore di eventi possa essere attenuato non significa che tali svantaggi non esistano, né significa che l'utilizzo di lambda come gestori di eventi sia una buona abitudine per entrare. –

risposta

1

Si scopre che non è così difficile, ma richiede una certa quantità di codice con un po 'di riflessione.

L'idea di base è quella di avvolgere il gestore in una classe generica parametrizzato dal tipo di evento

HandlerFor<T> : IDisposable where T : EventArgs. 

Questa classe ha quindi una funzione membro privata corrispondente alla richiesta firma gestore di eventi:

void Handle(object sender, T eventArgs) 

che si registrerà su costruzione, annullare la registrazione a disposizione e che invocherà il dato Action fornito nel suo costruttore ogni volta che si verifica l'evento.

Per nascondere i dettagli di implementazione ed esporre solo un IDisposable come handle per il controllo e l'annullamento del ciclo di vita del gestore eventi controllato, l'ho incluso in una classe EventHandlerRegistry. Ciò consentirà anche di aggiungere miglioramenti in futuro, se necessario, come la creazione di un delegato di fabbrica invece di chiamare ripetutamente lo Activator.CreateInstance() per creare le istanze HandleFor.

Ecco come si presenta:

public class EventHandlerRegistry : IDisposable 
{ 
    private ConcurrentDictionary<Type, List<IDisposable>> _registrations; 

    public EventHandlerRegistry() 
    { 
     _registrations = new ConcurrentDictionary<Type, List<IDisposable>>(); 
    } 

    public void RegisterHandlerFor(object target, EventInfo evtInfo, Action eventHandler) 
    { 
     var evtType = evtInfo.EventHandlerType.GetGenericArguments()[0]; 
     _registrations.AddOrUpdate(
      evtType, 
      (t) => new List<IDisposable>() { ConstructHandler(target, evtType, evtInfo, eventHandler) }, 
      (t, l) => { l.Add(ConstructHandler(target, evtType, evtInfo, eventHandler)); return l; }); 
    } 

    public IDisposable CreateUnregisteredHandlerFor(object target, EventInfo evtInfo, Action eventHandler) 
    { 
     var evtType = evtInfo.EventHandlerType.GetGenericArguments()[0]; 
     return ConstructHandler(target, evtType, evtInfo, eventHandler); 
    } 

    public void Dispose() 
    { 
     var regs = Interlocked.Exchange(ref _registrations, null); 
     if (regs != null) 
     { 
      foreach (var reg in regs.SelectMany(r => r.Value)) 
       reg.Dispose(); 
     } 
    } 

    private IDisposable ConstructHandler(object target, Type evtType, EventInfo evtInfo, Action eventHandler) 
    { 
     var handlerType = typeof(HandlerFor<>).MakeGenericType(evtType); 
     return Activator.CreateInstance(handlerType, target, evtInfo, eventHandler) as IDisposable; 
    } 

    private class HandlerFor<T> : IDisposable where T : EventArgs 
    { 
     private readonly Action _eventAction; 
     private readonly EventInfo _evtInfo; 
     private readonly object _target; 
     private EventHandler<T> _registeredHandler; 

     public HandlerFor(object target, EventInfo evtInfo, Action eventAction) 
     { 
      _eventAction = eventAction; 
      _evtInfo = evtInfo; 
      _target = target; 
      _registeredHandler = new EventHandler<T>(this.Handle); 
      _evtInfo.AddEventHandler(target, _registeredHandler); 
     } 

     public void Unregister() 
     { 
      var registered = Interlocked.Exchange(ref _registeredHandler, null); 
      if (registered != null) 
       // Unregistration is awkward: 
       // doing `RemoveEventHandler(_target, registered);` won't work. 
       _evtInfo.RemoveEventHandler(_target, new EventHandler<T>(this.Handle)); 
     } 

     private void Handle(object sender, T EventArgs) 
     { 
      if (_eventAction != null) 
       _eventAction(); 
     } 

     public void Dispose() 
     { 
      Unregister(); 
     } 
    } 
} 

Supporta pulito aggiunta e la rimozione di gestori di eventi in un modo abbastanza trasparente. Nota: questo non implementa ancora IDisposable nel modo raccomandato. Dovrai aggiungere i finalizzatori e Dispose(bool isFinalizing) tu stesso.

Questa mostra un esempio del suo utilizzo:

public class MyArgs1 : EventArgs 
{ 
    public string Value1; 
} 

public class MyEventSource 
{ 
    public event EventHandler<MyArgs1> Args1Event; 

    public EventInfo GetEventInfo() 
    { 
     return this.GetType().GetEvent("Args1Event"); 
    } 

    public void FireOne() 
    { 
     if (Args1Event != null) 
      Args1Event(this, new MyArgs1() { Value1 = "Bla " }); 
    } 
} 

class Program 
{ 
    public static void Main(params string[] args) 
    { 
     var myEventSource = new MyEventSource(); 
     using (var handlerRegistry = new EventHandlerRegistry()) 
     { 
      handlerRegistry.RegisterHandlerFor(
       myEventSource, 
       myEventSource.GetEventInfo(), 
       () => Console.WriteLine("Yo there's some kinda event goin on")); 
      handlerRegistry.RegisterHandlerFor(
       myEventSource, 
       myEventSource.GetEventInfo(), 
       () => Console.WriteLine("Yeah dawg let's check it out")); 

      myEventSource.FireOne(); 
     } 
     myEventSource.FireOne(); 
    } 
} 

Quando viene eseguito, si darà l'output di seguito:

output

2

Nell'esempio che ci hai fornito, si dovrebbe essere in grado di rimuovere solo l'esplicito delegato costruttore:

eventInfo.AddEventHandler(client, (obj, args) => task.Complete()); 

Lasciando C# dedurre il tipo delegato per voi, si dovrebbe creare esattamente il tipo delegato corretta necessaria per il parametro.

Se ciò non risolve il problema specifico, fornire a good, minimal, complete code example che riproduce il problema in modo affidabile, insieme a una spiegazione chiara e precisa del motivo per cui l'approccio di cui sopra non aiuta.


Per inciso, è difficile capire da quale piccolo codice hai postato, ma è insolito avere un metodo esplicito AddEventHandler(). Normalmente, una classe esporrà semplicemente un evento e userete la sintassi += per sottoscrivere un gestore di eventi.


EDIT:

Dai vostri commenti, ho capito che si sono tenuti per l'API per rispettare una firma evento fornito in modo dinamico. Personalmente, penso che questo tipo di design sia sciocco, ma presumo che tu sia bloccato con esso, presumibilmente a causa del design del framework Xamarin.

Prendendo l'obiettivo dichiarato rigorosamente — che è, dato un EventHandler esempio, produrre una nuova istanza delegato, il tipo di che corrisponde a un tempo di esecuzione fornito Type esempio — il seguente metodo dovrebbe funzionare per voi:

static Delegate CreateDelegate(Type type, EventHandler handler) 
{ 
    return (Delegate)type.GetConstructor(new [] { typeof(object), typeof(IntPtr) }) 
     .Invoke(new [] { handler.Target, handler.Method.MethodHandle.GetFunctionPointer() }); 
} 

Esempio di utilizzo:

eventInfo.AddEventHandler(client, 
    CreateDelegate(eventInfo.EventHandlerType, (obj, args) => task.Complete()); 

si potrebbe scrivere quanto sopra come un metodo di estensione per semplificare l'invocazione (non hai detto che tipo client è, quindi ho solo reso object per l'esempio):

public static void AddEventHandler(this EventInfo eventInfo, object client, EventHandler handler) 
{ 
     object eventInfoHandler = eventInfo.EventHandlerType 
      .GetConstructor(new[] { typeof(object), typeof(IntPtr) }) 
      .Invoke(new[] { handler.Target, handler.Method.MethodHandle.GetFunctionPointer() }); 

     eventInfo.AddEventHandler(client, (Delegate)eventInfoHandler); 
} 

Esempio di utilizzo:

eventInfo.AddEventHandler(client, (obj, args) => task.Complete()); 

Dare il metodo di estensione un nome diverso se siete preoccupati che ad un certo punto della propria AddEventHandler() API il metodo potrebbe cambiare in un modo che fa sì che il compilatore C# selezioni la sua implementazione anziché il metodo di estensione, o, naturalmente, se lo fa oggi (quanto sopra funzionerà assumendo solo un singolo metodo AddEventHandler() overload con il secondo parametro come Delegate, ma ancora & hellip in mancanza di un buono, minimo, completo codice di esempio Non posso garantire che funzionerà nel tuo codice; usare un nome unico potrebbe garantire che lo farà, a costo di esporre un po 'della "magia" :)).


Infine, dopo aver individuato una soluzione di lavoro, sono stato poi in grado di cercare Stack Overflow per domande simili e abbiamo trovato questo, di cui si potrebbe sostenere il proprio è un duplicato: Using reflection to specify the type of a delegate (to attach to an event)?

ho deciso di andare avanti e modifica la mia risposta qui piuttosto che semplicemente proponendo di chiudere la tua domanda, perché la risposta all'altra domanda non fornisce realmente quello che penso sia un esempio elegante o facile da usare.

+0

La riga che hai offerto sarebbe ideale, ma genera un'eccezione del compilatore: "Impossibile convertire l'espressione lambda in 'System.Delegate' perché non è un tipo delegato. Normalmente, vorrei usare la sintassi' + = ', tuttavia questo particolare situazione lo preclude: WCF genera un 'evento' unico per ogni chiamata asincrona che può essere fatta, ma quello che sto cercando di creare è un'utilità generica per invocare il metodo e allegare i callback al completamento. I'll look per ottenere un esempio di codice di riproduzione – Metameta

+0

@Metameta: Ciò significa che il metodo 'AddEventHandler()' utilizza 'Delegate' come tipo di parametro, piuttosto che qualsiasi tipo di delegato specifico alla fine sta tentando di eseguire il cast del parametro. Quindi la domanda è : cosa ti fa credere che dato un oggetto 'eventInfo', tu (non importa il compilatore) puoi dire che tipo di delegato è previsto)? Si prega di fare riferimento alla mia richiesta di fornire ** un buon esempio, _minimal_, _complete_ codice **.Solo con questo è possibile capire veramente la domanda e fornire una risposta che funzioni per te. –

+0

Il tipo di delegato da utilizzare è in realtà noto tramite EventInfo, che ha una proprietà EventHandlerType contenente il Tipo a cui il delegato deve conformarsi in fase di esecuzione. Tutto quello che mi piacerebbe fare è creare codice per rispondere all'evento, poiché non sono nemmeno interessato agli argomenti dell'evento; questo dovrebbe significare che voglio solo creare un gestore del tipo offerto, con il mio codice. – Metameta

Problemi correlati