2010-02-10 8 views
17

La didascalia è confusa. Vorrei chiarire un po ':C'è un modo per creare eventi indicizzati in C# (o qualche soluzione alternativa)?

Mi piacerebbe fornire eventi che dipendono da un parametro in modo che un osservatore possa decidere di ricevere eventi se accade qualcosa ad uno specifico "id". Potrebbe apparire come segue: (? Per esempio, come possiamo gestire disiscrizione)

public event EventHandler Foo (string id); 

Sono consapevole che questa sintassi è sbagliata in .NET 3.5, e sono anche consapevole del fatto che questa idea introduce ulteriore problema .

Come si deve risolvere questo problema? Ho pensato di usare qualcosa come:

public EventHandler Foo (string id); 

che è almeno la sintassi legale e potrebbe funzionare, ma ancora non sembra molto grande per me.

Edit: Sono non chiedendo di passaggio di argomenti alla funzione di callback. La mia idea è più simile a questo:

class Bleh 
{ 
    public event EventHandler Foo (string index); 

    private void RaiseEvents() // this is called by a timer or whatever 
    { 
     Foo["asdf"] (this, EventArgs.Empty); // raises event for all subscribers of Foo with a parameter of "asdf" 
     Foo["97"] (this, EventArgs.Empty); // same for all "97"-subscribers 
     // above syntax is pure fiction, obviously 
    } 
} 

// subscribe for asdf events via: 
Bleh x = new Bleh(); 
x.Foo["asdf"] += (s, e) => {}; 

Spiegazione
Dal momento che vi starete chiedendo il motivo per cui cerco di fare questo, mi spiego la mia situazione. Ho una classe che fornisce posizioni di determinati oggetti (ognuno identificato da una stringa ID).

Invece di fornire uno event EventHandler<PositionChangedEventArgs> elevato per QUALSIASI cambiamento di posizione, mi piacerebbe avere un evento per ogni oggetto (a cui si accede da un indice), in modo che gli osservatori possano ascoltare gli eventi solo per un ID specifico.

+0

Chiarimento (correggetemi se la mia interpretazione è sbagliata): Il controllo [per il quale gli ascoltatori di aumentare] è fatto prima di [se sollevare a quel ascoltatore] l'evento è licenziato, piuttosto che DOPO [l'abbonato lo ignora se non richiesto]. – Russell

+0

Esattamente. Il filtraggio deve essere eseguito alla fonte dell'evento. – mafu

risposta

16

Si potrebbe fare qualcosa di simile:

public class Foo 
{ 
    public class Bar 
    { 
     public event EventHandler PositionChanged; 

     internal void RaisePositionChanged() 
     { 
      var handler = PositionChanged; 
      if (handler != null) 
       handler(this, EventArgs.Empty); 
     } 
    } 

    private Dictionary<string, Bar> m_objects; 

    public Bar this[string id] 
    { 
     get 
     { 
      if (!m_objects.ContainsKey(id)) 
       m_objects.Add(id, new Bar()); 

      return m_objects[id]; 
     } 
    } 

    private void RaisePositionChanged(string id) 
    { 
     Bar bar; 
     if (m_objects.TryGetValue(id, out bar)) 
      bar.RaisePositionChanged(); 
    } 
} 

Poi per iscriversi un evento, che sarebbe stato semplice come questo:

Foo foo = new Foo(); 

foo["anId"].PositionChanged += YourHandler; 
+0

La tua classe 'Bar' avrebbe bisogno di un metodo' void interno RaiseEvents() 'per funzionare. – Gabe

+0

Infatti, ho aggiornato la risposta. –

+0

Bella idea, non molto codice aggiuntivo ma sintassi molto pulita per gli abbonati. – mafu

8

è necessario utilizzare una classe EventArgs-derivati ​​che include l'ID, e quindi utilizzare EventHandler<IdEventArgs> o qualsiasi altra cosa:

public class IdEventArgs : EventArgs 
{ 
    private readonly string id; 
    public string Id { get { return id; } } 

    public IdEventArgs(string id) 
    { 
     this.id = id; 
    } 
} 

public event Eventhandler<IdEventArgs> Foo; 

Quando si aumenta l'evento avrete bisogno di creare un'istanza di IdEventArgs, e poi l'abbonato può esaminarlo e decidere cosa fare con esso.

+0

Penso che tu stia parlando di qualcosa di diverso. Ho modificato la domanda per chiarire. – mafu

0

Vuoi dire qualcosa di simile

public class EventArgs<T> : EventArgs 
{ 
    private T _value; 
    public T Value 
    { 
     get { return this._value; } 
     protected set { this._value = value; } 
    } 

    public EventArgs(T value) 
    { 
     this.Value = value; 
    } 
} 


// ... 

public event EventHandler<EventArgs<string>> Foo; 

?

+0

No :) Vedi la domanda aggiornata. – mafu

+0

Sì, RX è quello che stai cercando, come ha detto MisterJames. – herzmeister

5

Ho appena iniziato a utilizzare Rx Framework ed è geniale. Penso che potrebbe essere quello che stai cercando.

http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx

di sottoscrizione e di non-abbonamento vengono gestiti nel quadro. È stato chiamato LINQ per gli eventi. È il "doppio matematico" di IEnumerable.

Cheers, -jc

+0

Potresti aggiungere un breve esempio su come usare Rx in modo che possa capire come si adatta alle mie esigenze? – mafu

+0

@mafutrct: Ho scritto una risposta con un codice di esempio per illustrare un modo in cui Rx può essere utilizzato per realizzare ciò che stai cercando. @MisterJames: È possibile che abbia fatto degli errori nei miei esempi. Sentiti libero di correggermi se è il caso. –

1

ho trovato fondamentalmente una più o meno elegante modo per risolvere questo problema:

utilizzare un dizionario di ID agli eventi. Accesso per aggiungere/rimuovere listener tramite metodi.

// ignore threadsafety and performance issues for now. 
private Dictionary<string, EventHandler> _Events = new Dictionary<string, EventHandler>(); 

private void AddId (string id) 
{ 
    _Events[id] = delegate { 
    }; 
} 

public void Subscribe (string id, EventHandler handler) 
{ 
    _Events[id] += handler; 
} 

public void Unsubscribe (string id, EventHandler handler) 
{ 
    _Events[id] -= handler; 
} 

private void Raise (string id) 
{ 
    _Events[id] (this, new EventArgs()); 
} 

static void Main (string[] args) 
{ 
    var p = new Program(); 

    p.AddId ("foo"); 
    p.Subscribe ("foo", (s, e) => Console.WriteLine ("foo")); 
    p.Raise ("foo"); 

    p.AddId ("bar"); 
    p.Subscribe ("bar", (s, e) => Console.WriteLine ("bar 1")); 
    p.Subscribe ("bar", (s, e) => Console.WriteLine ("bar 2")); 
    p.Raise ("bar"); 

    Console.ReadKey(); 
} 
+0

È anche possibile utilizzare un dizionario e utilizzare + = e - = per iscriversi e annullare l'iscrizione. Se l'ID è univoco, puoi semplicemente impostare il delegato invece di concatenare. –

+0

In effetti, bello, funziona molto bene. In realtà ho provato prima ma non ha funzionato, devo aver incasinato. Grazie! – mafu

+1

Mi piace per chiarezza di struttura e flessibilità. La principale differenza dalla soluzione di @JeffCyr è che include tutte le funzionalità Aggiungi, Sottoscrivi, Annulla sottoscrizione, Alza in una classe in modo da poter utilizzare la sintassi dell'indicizzatore. Uno deve scambiare la facilità di lettura con flessibilità di funzionalità. :) –

2

Non so se ho' d uso eventi in questo caso, ma non sono sicuro che sia davvero il problema più grande.

Se stai cercando di controllare gli abbonati, penso che sia meglio lasciare che gli abbonati gestiscano il filtro. Solo loro sanno cosa vogliono realmente filtrare, quindi mettendo il codice di filtraggio nella classe che emette gli eventi sembra non ottimale.

Vorrei provare a chiarire un po ', se posso ... Il codice per determinare se il destinatario A si interessa di un evento dall'emettitore B vive da qualche parte. Potrebbe sembrare logico inserirlo in B. Tuttavia, il problema si presenta quando ti rendi conto che devi considerare i destinatari C, D e E. Possono avere una logica complicata per determinare ciò a cui stanno a cuore (e potrebbe persino cambiare su itme). Mettendo tutta questa logica nel nostro emettitore (B) creerà una classe grande e goffa che è difficile da usare.

Un'altra opzione è quella di avere A la logica se desidera o meno l'evento internamente. Ciò localizza la logica su A, mantiene lo B pulito e facile da consumare da tutti gli altri. Tuttavia, il lato negativo di questo è che la logica di sottoscrizione non è utilizzabile da C se risulta essere lo stesso.

Ma se ci pensiamo davvero, abbiamo tre cose che accadono qui ... un evento emesso, filtraggio degli eventi ai destinatari e ricevuta/reazione agli eventi. Il Principio di Responsabilità Unica ci dice che una classe dovrebbe avere solo responsabilità, una ragione per cambiare. Includendo la logica di filtraggio in A o B, si ottiene ora due responsabilità e due motivi motivi per cambiare.

Quindi, quello che sarei tentato di fare in questo caso è creare un'altra classe, Q, che contiene la logica per il filtraggio degli eventi. Ora, né A o B ottengono la logica in più nel loro codice. C non è necessario ri-implementarlo. E, come bonus, ora possiamo facilmente collegare più filtri insieme per ottenere un comportamento del filtro complesso basato su componenti molto semplici.

+0

In questo caso, il filtro è molto semplice: ogni iscritto desidera ricevere eventi di solo 1 oggetto, identificato dal suo id. Non c'è davvero più logica di filtraggio, e il codice richiesto per creare una sottoscrizione dovrebbe essere un one-liner, quindi questo non dovrebbe essere un grosso problema. – mafu

+1

@mafutrct: In questo momento, forse, ma ho spesso trovato che le supposizioni che faccio a un certo punto finiscono per essere molto sbagliate in seguito. In ogni caso, non sto nemmeno parlando (davvero) di aggiungere alcun codice aggiuntivo - solo se la resonsabilità di "questo destinatario si preoccupa di questo messaggio" appartiene al destinatario o al mittente. E 'un one-liner, vero, ma è un one-liner in entrambe le classi. – kyoryu

0

Implementato come una singola classe, con una semplice API.

// subscribe to an event 
eventsource.AddHandler("foo", MyEventHandler); 

// unsubscribe 
eventsource.RemoveHandler("foo", MyEventHandler); 

// raise event for id 
eventsource.RaiseEvent("foo"); 

public class EventSource 
{ 
    Dictionary<string,List<EventHandler>> handlers = new Dictionary<string,List<EventHandler>>(); 

    public void AddHandler(string id, EventHandler handler) 
    { 
     if (!handlers.ContainsKey(id)) { 
      handlers[id] = new List<EventHandler>(); 
     } 
     handlers[id].Add(handler); 
    } 

    public void RemoveHandler(string id, EventHandler handler) 
    { 
     if (handlers.ContainsKey(id)) { 
      handlers[id].Remove(handler); 
     } 
    } 

    public void RaiseEvent(string id) 
    { 
     if (handlers.ContainsKey(id)) { 
      foreach(var h in handlers[id]) { 
       h(this, EventArgs.Empty); 
      } 
     }  
    } 
} 
+0

Sì, ma consultare http://stackoverflow.com/questions/2237927/is-there-any-way-to-create-indexed-events-in-c-or-some-workaround/2244501#2244501 per una versione più semplice . – mafu

5

Credo Reactive Extensions for .NET è esattamente quello che stai cercando.

L'idea * è questo:

In primo luogo, definire una classe che deriva da EventArgs e comprende le informazioni che si desidera (in particolare, qualunque "indice" che aveva in mente).Qualcosa di simile a questo:

public class IndexedEventArgs : EventArgs { 
    public string Index { get; private set; } 

    public IndexedEventArgs(string index) { 
     Index = index; 
    } 

    // ... 
} 

Avanti, per la classe che sarà la raccolta di eventi, implementare un singolo evento utilizzando EventHandler<TEventArgs> e questa classe appena definito. All'interno di questa definizione di classe, creare un oggetto che implementa IObservable come segue:

public class ClassWithIndexedEvents { 
    public event EventHandler<IndexedEventArgs> IndexedEvent; 

    public IObservable Events { get; private set; } 

    public ClassWithIndexedEvents() { 
     // yeah, this feels a little weird, but it works 
     Events = Observable.FromEvent<IndexedEventArgs>(this, "IndexedEvent"); 
    } 

    // ... 
} 

Ora, nel codice in cui si desidera sottoscrivere solo gli eventi che corrispondono a un determinato indice, è possibile filtrare la vostra proprietà Events esattamente nello stesso modo si farebbe un IEnumerable:

// code mangled to fit without scrolling 
public IDisposable CreateSubscription(
    string eventIndex, 
    Action<IEvent<IndexedEventArgs>> handler) { 

    return Events.Where(e => e.Index == eventIndex).Subscribe(handler); 
} 

si noti che il metodo restituisce un oggetto SubscribeIDisposable; questa è la tua chiave per il successivo annullamento dell'iscrizione di dall'evento filtrato a cui ti sei appena iscritto. Il codice è abbastanza evidente:

var fooSubscription = objectWithIndexedEvents.CreateSubscription(
    "foo", 
    e => DoSomething(e) 
); 

// elsewhere in your code 
fooSubscription.Dispose(); 

* Disclaimer: io scrivo tutto questo codice più o meno a memoria di come funziona Rx; Non l'ho provato, dal momento che non ho Rx installato sulla macchina che sto attualmente usando. Posso controllare domani su una macchina diversa per assicurarmi che sia scritta correttamente; per ora dovrebbe almeno servire da illustrazione per darti un'idea di come funziona Rx. Per saperne di più puoi sempre consultare i tutorial Rx online.

+0

+1 interessante. È un po 'triste che debba essere usata una terza parte. – mafu

+0

@mafutrct: Ha, suppongo che sia un modo di guardarlo. Considerando che si tratta di un progetto Microsoft, però, non lo vedo come "3rd party" più di quanto lo stesso framework .NET sia di terze parti (che è ** **, ovviamente, ma non ho mai pensato, "È un po 'triste, devo usare questa cosa di terze parti" .NET "per questo progetto ..."). Ovviamente potresti scrivere la tua libreria che riproduce ciò che Rx fa, ma perché dovresti? –

+0

Sì, certo, è meglio usare meno librerie per me. Fare le cose in modo facile ed efficace in "codice nativo" è ovviamente sempre la migliore opzione - _ se è possibile. – mafu

2

Ho preparato un esempio completo. Si può usare in questo modo:

 eventsSubscriptions["1"].EventHandler = new EventHandler(this.Method1); 
     eventsSubscriptions["2"].EventHandler = new EventHandler(this.Method2); 
     eventsSubscriptions["3"].EventHandler = new EventHandler(this.Method3); 

     Boss Boss1 = new Boss("John Smith"); 
     Boss Boss2 = new Boss("Cynthia Jameson"); 

     Employed Employed1 = new Employed("David Ryle"); 
     Employed Employed2 = new Employed("Samantha Sun"); 
     Employed Employed3 = new Employed("Dick Banshee"); 

     // Subscribe objects to Method 1 
     eventsSubscriptions["1"].Subscribe(Boss1); 
     eventsSubscriptions["1"].Subscribe(Employed1); 

     // Subscribe objects to Method 2 
     eventsSubscriptions["2"].Subscribe(Boss2); 
     eventsSubscriptions["2"].Subscribe(Employed2); 

     // Subscribe objects to Method 3 
     eventsSubscriptions["3"].Subscribe(Employed3); 

Quindi, è possibile chiamare RaiseAllEvents() metodi, e questo è l'output della console:

  • Metodo 1 rialzata con Boss John Smith
  • Metodo 1 sollevato con dipendenti David Ryle
  • Metodo 2 sollevato con Boss Cynthia Jameson
  • Metodo 2 rialzato con dipendenti Samantha Sun
  • Metodo 3 generato con Employee Dick Banshee

Nelle righe seguenti, incollerò il codice di tutte le classi coinvolte. Con un po 'di pazienza e copia/incolla, sarai in grado di provarlo = P Spero che ti aiuti.

--- --- Il Codice

principale

namespace MyExample 
{ 
    public class Program 
    { 

     static void Main(string[] args) 
     { 
      SomeExampleClass someExampleInstance = new SomeExampleClass(); 

      someExampleInstance.SuscribeObjects();    
      someExampleInstance.RaiseAllEvents();    

      Console.ReadLine(); 
     } 


    } 
} 

Classe persona

namespace MyExample 
{ 
    public abstract class Person 
    { 
     protected string name; 

     public Person(string name) 
     { 
      this.name = name; 
     } 

     public string Name 
     { 
      get 
      { 
       return name; 
      } 
      set 
      { 
       name = value; 
      } 
     } 

     public override string ToString() 
     { 
      return (this.GetType().Name + " " + name); 
     } 
    } 
} 

Classe Boss

namespace MyExample 
{ 
    public class Boss : Person 
    { 
     public Boss(string name) 
      : base(name) 
     { } 
    } 
} 

dipendenti

namespace MyExample 
{ 
    public class Employee : Person 
    { 
     public Employee(string name) 
      : base(name) 
     { } 
    } 
} 

Classe SomeExampleClass

namespace MyExample 
{ 
    public class SomeExampleClass 
    { 

     private EventsSubscriptions eventsSubscriptions = new EventsSubscriptions(); 

     private void Method1(object sender, System.EventArgs e) 
     { 
      Console.WriteLine("Method 1 raised with " + sender.ToString()); 
     } 

     private void Method2(object sender, System.EventArgs e) 
     { 
      Console.WriteLine("Method 2 raised with " + sender.ToString()); 
     } 

     private void Method3(object sender, System.EventArgs e) 
     { 
      Console.WriteLine("Method 3 raised with " + sender.ToString()); 
     } 

     public void SuscribeObjects() 
     {    
      eventsSubscriptions["1"].EventHandler = new EventHandler(this.Method1); 
      eventsSubscriptions["2"].EventHandler = new EventHandler(this.Method2); 
      eventsSubscriptions["3"].EventHandler = new EventHandler(this.Method3); 

      Boss Boss1 = new Boss("John Smith"); 
      Boss Boss2 = new Boss("Cynthia Jameson"); 

      Employee Employee1 = new Employee("David Ryle"); 
      Employee Employee2 = new Employee("Samantha Sun"); 
      Employee Employee3 = new Employee("Dick Banshee"); 

      // Method 1 
      eventsSubscriptions["1"].Subscribe(Boss1); 
      eventsSubscriptions["1"].Subscribe(Employee1); 

      //// Method 2 
      eventsSubscriptions["2"].Subscribe(Boss2); 
      eventsSubscriptions["2"].Subscribe(Employee2); 

      //// Method 3 
      eventsSubscriptions["3"].Subscribe(Employee3); 

     } 

     public void RaiseAllEvents() 
     { 
      eventsSubscriptions.RaiseAllEvents(); 
     } 

    } 
} 

EventsSubscriptions Classe

namespace MyExample 
{ 
    public class EventsSubscriptions 
    { 
     private Dictionary<string, Subscription> subscriptions = new Dictionary<string, Subscription>(); 

     public Subscription this[string id] 
     { 
      get 
      { 
       Subscription subscription = null; 

       subscriptions.TryGetValue(id, out subscription); 

       if (subscription == null) 
       {      
        subscription = new Subscription(); 
        subscriptions.Add(id, subscription); 
       } 

       return subscription; 

      } 
     } 

     public void RaiseAllEvents() 
     { 
      foreach (Subscription subscription in subscriptions.Values) 
      { 
       Subscription iterator = subscription; 

       while (iterator != null) 
       { 
        iterator.RaiseEvent(); 
        iterator = iterator.NextSubscription; 
       } 
      } 
     } 


    } 
} 

classe di sottoscrizione

namespace MyExample 
{ 
    public class Subscription 
    { 
     private object suscribedObject; 
     private EventHandler eventHandler; 
     private Subscription nextSubscription; 

     public object SuscribedObject 
     { 
      set 
      { 
       suscribedObject = value; 
      } 
     } 

     public EventHandler EventHandler 
     { 
      set 
      { 
       eventHandler = value; 
      } 
     } 

     public Subscription NextSubscription 
     { 
      get 
      { 
       return nextSubscription; 
      } 
      set 
      { 
       nextSubscription = value; 
      } 
     } 

     public void Subscribe(object obj) 
     { 

      if (suscribedObject == null) 
      { 
       suscribedObject = obj; 
      } 
      else 
      { 
       if (nextSubscription != null) 
       { 
        nextSubscription.Subscribe(obj); 
       } 
       else 
       { 
        Subscription newSubscription = new Subscription(); 
        newSubscription.eventHandler = this.eventHandler; 
        nextSubscription = newSubscription; 
        newSubscription.Subscribe(obj); 
       } 
      } 
     } 

     public void RaiseEvent() 
     { 
      if (eventHandler != null) 
      { 
       eventHandler(suscribedObject, new System.EventArgs()); 
      } 
     } 
    } 
} 
+0

+1 bel esempio, ma il design sembra in qualche modo strano (rispetto all'idea di Jeff). – mafu

+1

Grazie a @mafutrct. So che il mio esempio è ampio, ma vedi che è molto flessibile. Puoi iscrivere oggetti a tutti i metodi che desideri, non solo a PositionChanged. =) – Javier

0

Come circa l'attuazione di INotifyPropertyChanged invece?

E poi ...

protected void NotifyPropertyChanged(String propertyName) 
{ 
    if (PropertyChanged != null) 
     PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
} 


private void OnSourcePropertyChanged(Object sender, PropertyChangedEventArgs eventArgs) 
{ 
    if (eventArgs.PropertyName == "InterestingName") 
    { 
     // TODO: 
    } 
} 
+0

Questo è praticamente lo stesso di JonSkeet già proposto. Risolve il problema più o meno, ma non è il modo in cui dovrebbe essere: mi piacerebbe filtrare gli eventi alla fonte. – mafu

+0

Per le implementazioni ottimali, è necessario un dizionario all'origine o alle destinazioni, se si mantiene il dizionario sui target che consente di connettere un gestore ad alcuni eventi diversi quando richiesto. –

Problemi correlati