2009-06-01 7 views
73

en double de: How to ensure an event is only subscribed to once et Has an event handler already been added?modèle C# pour empêcher un gestionnaire d'événements accroché deux fois

J'ai un singleton qui fournit un service et mes cours crochet dans certains événements sur, parfois une classe hooking deux fois à l'événement puis est appelé deux fois. Je suis à la recherche d'un moyen classique pour éviter que cela se produise. en quelque sorte je dois vérifier si j'ai déjà accroché à cet événement ...

Répondre

120

Implémentez explicitement l'événement et vérifiez la liste d'invocation. Vous devrez également vérifier null:

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

En utilisant le code ci-dessus, si un appelant est abonné à l'événement plusieurs fois, il sera tout simplement ignoré.

+13

Vous devez utiliser le System.Linq en utilisant. –

+0

Juste pour clarifier le commentaire de Hermann; vous devez inclure l'espace de noms 'System.Linq' en ajoutant 'using System.Linq' à votre classe ou à votre espace de noms actuel. –

+0

Fascinant. LINQ est encore assez nouveau pour moi que je dois le rechercher et être rappelé cela signifie Language Integrated Query ... puis se demander ce que cela a à voir avec EventHandlers et leur InvocationList? – fortboise

12

Vous devez implémenter les accesseurs ajouter et supprimer sur l'événement, puis vérifier la liste cible du délégué, ou stocker les cibles dans un liste.

Dans la méthode add, vous pouvez utiliser la méthode Delegate.GetInvocationList pour obtenir la liste des cibles déjà ajoutées au délégué. Puisque les délégués sont définis pour comparer des égales s'ils sont liés à la même méthode sur le même objet cible, vous pouvez probablement parcourir cette liste et comparer, et si vous n'en trouvez aucun qui soit égal, vous ajoutez le nouveau .

est ici exemple de code, la compilation en application 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"); 
     } 
    } 
} 

Sortie:

tc_Test called 

(c.-à-une seule fois.)

+0

S'il vous plaît ignorer mon commentaire, j'ai oublié le Linq en utilisant. –

+0

Solution la plus propre (mais pas la plus courte). – Shimmy

0

ont votre objet singleton vérifier sa liste de qui il notifie et seulement appeler une fois si elle est dupliquée. Alternativement, si possible, rejeter la demande de pièce jointe.

17

Vous devriez vraiment gérer ce au niveau du puits et non le niveau de la source. C'est-à-dire, ne prescrivez pas de logique de gestionnaire d'événements à la source de l'événement - laissez cela aux gestionnaires (les puits) eux-mêmes.

En tant que développeur d'un service, qui êtes-vous pour dire que les récepteurs ne peuvent s'inscrire qu'une seule fois? Et s'ils veulent s'inscrire deux fois pour une raison quelconque? Et si vous essayez de corriger les bogues dans les puits en modifiant la source, c'est encore une bonne raison de corriger ces problèmes au niveau de l'évier. Je suis sûr que vous avez vos raisons; une source d'événement pour laquelle les récepteurs dupliqués sont illégaux n'est pas insondable. Mais peut-être devriez-vous envisager une architecture alternative qui laisse intacte la sémantique d'un événement.

+0

Ceci est une excellente justification d'ingénierie pour [cette solution] (http://stackoverflow.com/a/1104269/3367144), qui résout le problème du côté récepteur d'événement (abonné/consommateur/observateur/gestionnaire) au lieu de la source côté. – kdbanman

140

Que diriez-vous juste enlever le premier événement avec -=, si elle ne se trouve pas une exception n'est pas jeté

/// -= 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

Merci. Cela est très utile dans le même scénario (WebBrowser + HtmlElementEventHandler). Merci de le signaler – Odys

+0

Cela devrait être la réponse acceptée car elle est simple et ne nécessite aucune implémentation personnalisée. @LoxLox montre le même modèle qu'une implémentation. Je n'ai pas testé, donc je prends les commentaires au mot. Très agréable. – Rafe

+3

+1 Je suppose que cela dépend du programmeur, mais je dirais que c'est le meilleur (pas le «plus simple» d'exiger que le développeur final fasse cela, mais ce n'est pas la faute du générateur d'événements que l'abonné ne peut empêcher abonnements multiples, donc, faites-leur comprendre la suppression, etc ... d'ailleurs pourquoi empêcher quelqu'un de s'abonner plus d'une fois au même gestionnaire si le vouloir?) –

6

Microsoft Reactive Extensions (Rx) framework peut également être utilisé pour faire « souscrire qu'une seule fois ».

Étant donné un événement de souris foo.Clicked, voici comment vous abonner et recevoir une seule invocation:

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

En plus de fournir la fonctionnalité « subscribe une fois », l'approche RX offre la possibilité de composer des événements ensemble ou filtrer les événements. C'est assez chouette.

+0

Bien que techniquement correct, il répond à la mauvaise question. – AlexFoxGill

0

Dans silverlight vous devez dire e.Handled = true; dans le code de l'événement.

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

Veuillez me cocher si cela vous aide.

1

Créez une action au lieu d'un événement. Votre classe peut ressembler à:

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

Je l'ai testé chaque solution et le meilleur (performance considérant) est:

private EventHandler _foo; 
public event EventHandler Foo { 

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

Pas Linq utilisant nécessaire. Pas besoin de vérifier null avant d'annuler un abonnement (voir MS EventHandler pour plus de détails). Pas besoin de se souvenir de faire la désinscription partout.