2009-06-01 15 views
73

duplikat: How to ensure an event is only subscribed to once i Has an event handler already been added?C# wzór aby zapobiec obsługi zdarzeń uzależniony dwukrotnie

Mam singleton, który zapewnia pewne usługi i moje zajęcia hak do niektórych wydarzeń na nim, czasami klasa jest zaczepiając dwa razy na imprezy a następnie zostaje wywołany dwa razy. Szukam klasycznego sposobu, aby temu zapobiec. jakoś muszę sprawdzić, czy już podpinałem się na to wydarzenie ...

Odpowiedz

120

Należy jawnie zaimplementować zdarzenie i sprawdzić listę wywołań. Trzeba także sprawdzić dla wartości 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; 
    } 
} 

Stosując powyższy kod, jeśli rozmówca zgadza się przypadku, gdy wiele razy, to będzie po prostu ignorowana.

+13

Musisz użyć System.Linq używając. –

+0

Wystarczy wyjaśnić komentarz Hermanna; musisz dodać przestrzeń nazw "System.Linq", dodając "using System.Linq" do swojej klasy lub aktualnej przestrzeni nazw. –

+0

Fascynujące. LINQ jest dla mnie wystarczająco nowy, że muszę go sprawdzić i przypomnieć, że to znaczy Zintegrowane Zapytanie językowe ... i zastanawiam się, co to ma wspólnego z EventHandlers i ich InvocationList? – fortboise

12

Musisz zaimplementować add and remove accessors na zdarzeniu, a następnie sprawdzić listę docelową delegata lub przechowywać cele w lista.

W metodzie dodawania można użyć metody Delegate.GetInvocationList, aby uzyskać listę celów już dodanych do uczestnika.

Ponieważ delegaci są zdefiniowani tak, aby porównać je równomiernie, jeśli są połączeni z tą samą metodą na tym samym obiekcie docelowym, prawdopodobnie można uruchomić tę listę i porównać, a jeśli nie znajdziesz równego porównania, dodaj nowy .

Oto przykładowy kod, skompilować jako aplikacji konsoli:

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

wyjściowa:

tc_Test called 

(czyli tylko raz.)

+0

Proszę zignorować mój komentarz, zapomniałem używać Linq. –

+0

Najczystsze rozwiązanie (choć nie najkrótsze). – Shimmy

0

mieć swój Singleton obiekt sprawdzić to lista, która powiadamia i wywołaj tylko raz, jeśli jest duplikowany. Ewentualnie, jeśli to możliwe, odrzuć żądanie załączenia zdarzenia.

17

Naprawdę powinien się tym zająć na poziomie zlewu, a nie poziom źródła. Oznacza to, że nie przepisuj logiki obsługi zdarzeń w źródle zdarzenia - pozostaw to samemu modułowi obsługi (zlewom).

Jako twórca usługi, kto może powiedzieć, że pochłaniacze można zarejestrować tylko raz? Co, jeśli z jakiegoś powodu chcą się zarejestrować dwukrotnie? A jeśli próbujesz poprawić błędy w zlewach, modyfikując źródło, to znowu jest to dobry powód, aby poprawić te problemy na poziomie zlewu.

Jestem pewien, że masz swoje powody; źródło zdarzeń, dla którego podwójne pochłaniacze są nielegalne, nie jest niewyobrażalne. Ale być może powinieneś rozważyć alternatywną architekturę, która pozostawia nietkaną semantykę zdarzenia.

+0

Jest to doskonałe uzasadnienie techniczne dla tego rozwiązania (http://stackoverflow.com/a/1104269/3367144), które rozwiązuje problem po stronie zlewu zdarzeń (strona subskrybent/konsument/obserwator/przewodnik) zamiast źródła bok. – kdbanman

140

Jak o pierwszy właśnie usunięcie zdarzenia z -=, jeśli nie zostanie znaleziony wyjątek nie zostanie rzucony

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

Dziękuję. To było bardzo przydatne w tym samym scenariuszu (WebBrowser + HtmlElementEventHandler). Dzięki za wskazanie tego – Odys

+0

To powinna być akceptowana odpowiedź, ponieważ jest prosta i nie wymaga niestandardowej implementacji. @LoxLox pokazuje ten sam wzór, co implementacja. Nie testowałem, więc biorę komentarze pod ich słowa. Bardzo dobrze. – Rafe

+3

+1 Zgaduję, że to zależy od programisty, ale powiedziałbym, że to jest najlepsze (nie "najczystsze", aby wymagać od tego końcowego dewelopera, ale nie jest to wina awarii generatora zdarzeń, której subskrybent nie może zapobiec. wielokrotne subskrypcje, aby dowiedzieć się, jak je usunąć, itp. ... poza tym, dlaczego ktoś nie chce subskrybować tego samego handlarza więcej niż jeden raz, jeśli tego chce?) –

6

Microsoft Reactive Extensions (Rx) framework może być również użyty do „subskrybować tylko raz”.

Biorąc zdarzenie myszy foo.Clicked, oto jak subskrybować i odbierać tylko jedno wezwanie:

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

Ponadto, aby zapewnić „subskrybować raz” funkcjonalność, podejście RX oferuje możliwość komponowania zdarzeń razem lub filtruj zdarzenia. To całkiem sprytne.

+0

Chociaż jest to technicznie poprawne, odpowiada na niewłaściwe pytanie. – AlexFoxGill

0

W silverlight trzeba powiedzieć e.Handled = true; w kodzie zdarzenia.

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

Proszę zaznaczyć mnie, jeśli to pomaga.

1

Utwórz działanie zamiast wydarzenia. Klasa może wyglądać następująco:

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

Przetestowałem każde rozwiązanie i najlepszy (wydajność rozważa) wynosi:

private EventHandler _foo; 
public event EventHandler Foo { 

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

Nie LINQ korzystając wymagane. Nie ma potrzeby sprawdzania wartości zerowej przed anulowaniem subskrypcji (szczegóły: MS EventHandler). Nie trzeba pamiętać o tym, aby wszędzie zrezygnować z subskrypcji.

Powiązane problemy