2009-06-01 11 views
73

duplicati di: How to ensure an event is only subscribed to once e Has an event handler already been added?C# modello per evitare che un gestore di eventi agganciato due volte

Ho un Singleton che fornisce qualche servizio e le mie lezioni gancio in alcuni eventi su di esso, a volte una classe è agganciando due volte per l'evento e poi viene chiamato due volte. Sto cercando un modo classico per evitare che ciò accada. in qualche modo ho bisogno di verificare se mi sono già collegato a questo evento ...

risposta

120

Implementare esplicitamente l'evento e controllare l'elenco di chiamate. Avrete anche bisogno di controllare per nulla:

using System.Linq; // Required for the .Contains call below: 

... 

private EventHandler foo; 
public event EventHandler Foo 
{ 
    add 
    { 
     if (foo == null || !foo.GetInvocationList().Contains(value)) 
     { 
      foo += value; 
     } 
    } 
    remove 
    { 
     foo -= value; 
    } 
} 

Utilizzando il codice di cui sopra, se un chiamante sottoscrive le molteplici orari degli eventi, esso viene semplicemente ignorato.

+13

È necessario utilizzare System.Linq utilizzando. –

+0

Giusto per chiarire il commento di Hermann; devi includere lo spazio dei nomi 'System.Linq' aggiungendo 'using System.Linq' alla tua classe o spazio dei nomi corrente. –

+0

Affascinante. LINQ è ancora abbastanza nuovo per me da doverlo cercare e ricordare che significa Language Integrated Query ... e poi chiedersi che cosa ha a che fare con EventHandlers e la loro InvocationList? – fortboise

12

È necessario implementare l'aggiunta e rimuovere gli utenti dall'evento, quindi controllare l'elenco di destinazione del delegato o archiviare gli obiettivi in ​​un elenco.

Con il metodo add, è possibile utilizzare il metodo Delegate.GetInvocationList per ottenere un elenco degli obiettivi già aggiunti al delegato.

Poiché i delegati sono definiti per essere uguali se sono collegati allo stesso metodo sullo stesso oggetto di destinazione, è probabile che si possa scorrere l'elenco e confrontarsi, e se non si trova nessuno che sia uguale, si aggiunge quello nuovo .

Ecco il codice di esempio, compilare come applicazione console:

using System; 
using System.Linq; 

namespace DemoApp 
{ 
    public class TestClass 
    { 
     private EventHandler _Test; 

     public event EventHandler Test 
     { 
      add 
      { 
       if (_Test == null || !_Test.GetInvocationList().Contains(value)) 
        _Test += value; 
      } 

      remove 
      { 
       _Test -= value; 
      } 
     } 

     public void OnTest() 
     { 
      if (_Test != null) 
       _Test(this, EventArgs.Empty); 
     } 
    } 

    class Program 
    { 
     static void Main() 
     { 
      TestClass tc = new TestClass(); 
      tc.Test += tc_Test; 
      tc.Test += tc_Test; 
      tc.OnTest(); 
      Console.In.ReadLine(); 
     } 

     static void tc_Test(object sender, EventArgs e) 
     { 
      Console.Out.WriteLine("tc_Test called"); 
     } 
    } 
} 

uscita:

tc_Test called 

(cioè solo una volta.)

+0

Si prega di ignorare il mio commento, ho dimenticato il Linq utilizzando. –

+0

Soluzione più pulita (anche se non la più corta). – Shimmy

0

fare controllare il vostro oggetto Singleton è lista di chi lo comunica e chiama solo una volta se duplicato. In alternativa, se possibile, respingere la richiesta di collegamento all'evento.

17

Si dovrebbe gestire questo a livello di lavandino e non il livello della sorgente. Cioè, non prescrivere la logica del gestore di eventi alla fonte dell'evento - lasciatelo ai gestori (i lavandini) stessi.

In qualità di sviluppatore di un servizio, chi può dire che i sink si possono registrare solo una volta? Cosa succede se vogliono registrarsi due volte per qualche motivo? E se stai cercando di correggere i bug nei sink modificando la fonte, è di nuovo una buona ragione per correggere questi problemi a livello di sink.

Sono sicuro che hai le tue ragioni; una fonte di eventi per cui i sink duplicati sono illegali non è insondabile. Ma forse dovresti considerare un'architettura alternativa che lasci intatta la semantica di un evento.

+0

Questa è una giustificazione ingegneristica eccellente per [questa soluzione] (http://stackoverflow.com/a/1104269/3367144), che risolve il problema sul lato evento sink (subscriber/consumer/observer/handler) al posto della fonte lato. – kdbanman

140

Come circa appena la rimozione del primo evento con -=, se non viene trovato un'eccezione non viene lanciata

/// -= Removes the event if it has been already added, this prevents multiple firing of the event 
((System.Windows.Forms.WebBrowser)sender).Document.Click -= new System.Windows.Forms.HtmlElementEventHandler(testii); 
((System.Windows.Forms.WebBrowser)sender).Document.Click += new System.Windows.Forms.HtmlElementEventHandler(testii); 
+0

Grazie. Questo è stato molto utile nello stesso scenario (WebBrowser + HtmlElementEventHandler). Grazie per aver segnalato questo – Odys

+0

Questa dovrebbe essere la risposta accettata in quanto è semplice e non richiede implementazione personalizzata. @LoxLox mostra lo stesso schema di un'implementazione. Non ho provato, quindi sto prendendo i commenti sulla loro parola. Molto bella. – Rafe

+3

+1 Suppongo che dipenda dal programmatore, ma direi che questo è il migliore (non il "più accurato" da richiedere allo sviluppatore finale di farlo forse, ma non è colpa del generatore di eventi che l'abbonato non può impedire più abbonamenti, quindi, fagli capire la rimozione, ecc ... inoltre, perché impedire a qualcuno di sottoscrivere lo stesso gestore più di una volta se lo desideri?) –

6

di Reactive Extensions (Rx) framework Microsoft può anche essere usato per fare "subscribe solo una volta".

dato un evento del mouse foo.Clicked, ecco come per sottoscrivere e ricevere una sola invocazione:

Observable.FromEvent<MouseEventArgs>(foo, "Clicked") 
    .Take(1) 
    .Subscribe(MyHandler); 

... 

private void MyHandler(IEvent<MouseEventArgs> eventInfo) 
{ 
    // This will be called just once! 
    var sender = eventInfo.Sender; 
    var args = eventInfo.EventArgs; 
} 

Oltre a fornire funzionalità "subscribe una volta", l'approccio RX offre la possibilità di comporre gli eventi insieme o filtrare eventi. È abbastanza elegante.

+0

Sebbene questo sia tecnicamente corretto, risponde alla domanda sbagliata. – AlexFoxGill

0

In silverlight è necessario dire e.Handled = true; nel codice dell'evento.

void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 
{ 
    e.Handled = true; //this fixes the double event fire problem. 
    string name = (e.OriginalSource as Image).Tag.ToString(); 
    DoSomething(name); 
} 

Si prega di barrare me se questo aiuta.

1

Creare un'azione anziché un evento. La classe può sembrare:

public class MyClass 
{ 
       // sender arguments  <-----  Use this action instead of an event 
    public Action<object, EventArgs> OnSomeEventOccured; 

    public void SomeMethod() 
    { 
      if(OnSomeEventOccured!=null) 
       OnSomeEventOccured(this, null); 
    } 

} 
20

ho testato ogni soluzione e la migliore (prestazioni considerando) è:

private EventHandler _foo; 
public event EventHandler Foo { 

    add { 
     _foo -= value; 
     _foo += value; 
    } 
    remove { 
     _foo -= value; 
    } 
} 

No LINQ utilizzando necessaria. Non è necessario verificare la presenza di null prima di annullare un abbonamento (per dettagli vedi MS EventHandler). Non c'è bisogno di ricordare di fare la disiscrizione ovunque.

Problemi correlati