Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hooking to an event of all objects in a list

Problem: I find myself often wanting to handle an event on a collection of objects. As objects are added and removed from the collection, each object must be hooked or unhooked. I find this to be tedious and repetetive to design each class that does this with the same event hooking code.

Desired Solution: So, I'm trying to come up with something like an EventBindingList, which contains hookable objects, and allows the user to hook multiple objects at once, and to add and remove objects in the list.

In order to keep it generic, it is necessary to use Reflection. In the constructor of the list, the user can specify by EventInfo or by Event name what event is being hooked. This seemed like the simplest way.

    private EventInfo _info;

    public EventBindingList(string EventName)
    {
        _info = typeof(T).GetEvents().Where(e => e.Name == EventName).First();
    }

    public EventBindingList(EventInfo info)
    {
        _info = info;
    }

I've tried a couple of approaches, but I continue to have problems with the differences between methods, delegates, lambdas, and EventHandlers.

Failed Solution 1:

One solution I tried which did not work was to use a custom event accessor. This would be an event on the list containing the objects to be hooked. This is because, when adding an EventHandler, an ArgumentException is thrown: Object of type 'System.EventHandler' cannot be converted to type 'ExternalProject.CustomEventHandler'. I tried casting the EventHandler to the correct type (using generic type arguments, since this is an external project's event handler), but the cast would fail.

    public event EventHandler ElementEvent
    {
        add
        {
            _handlers.Add(value);
            foreach (T t in this)
            {
                _info.AddEventHandler(t, value);
            }
        }
        remove
        {
            foreach (T t in this)
            {
                _info.RemoveEventHandler(t, value);
            }
            _handlers.Remove(value);
        }
    }

Failed Solution 2:

I have not found a good way to have the list itself handle the event, and then call a delegate for any subscribers. I discovered that attempting to use reflection to add an event handler requires a delegate. In my tests, I could find no way to preserve the parameters of the event, and pass those along to the subscriber as well.

Request: Are there any other ideas for how this may be accomplished?

like image 778
Nathan Avatar asked Jul 11 '11 17:07

Nathan


People also ask

How do you invoke an event?

You need to first get the field that represents the event on the type as a MulticastDelegate , then get its invocationlist and from there you can dynamically invoke each method individually. The answer to this MSDN question shows how to do it.

What is EventArgs E in C#?

EventArgs e is a parameter called e that contains the event data, see the EventArgs MSDN page for more information. Object Sender is a parameter called Sender that contains a reference to the control/object that raised the event.

What happens if an event occurs and there is no event handler to respond to the event?

TF: if an event occurs and there is not event handler to respond to that event, the event ins ignored.


1 Answers

Edit:

public abstract class ManagedEventCollection<T,TEventArgs> : IList<T>
{
   private EventInfo m_event;
   public ManagedEventCollection(string eventName)
   {
      m_list = new ObservableCollection<T> ();
      m_list.CollectionChanged += CollectionChanged;
      m_event = typeof (T).GetEvent (eventName);
   }
   // Add/Remove/Indexer/Clear methods alter contents of m_list.

   public EventHandler<TEventArgs> Handler{get;set;}

   protected abstract void OnItemAdded(T item);
   protected abstract void OnItemRemoved(T item);

   private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs ea)
   {
      foreach (T item in ea.NewItems)
      {
         m_event.AddEventHandler (
            item, 
            Delegate.CreateDelegate (m_event.EventHandlerType, item, Handler.Method));
      }
      foreach (T item in ea.OldItems)
      {
         m_event.RemoveEventHandler (
            item, 
            Delegate.CreateDelegate (m_event.EventHandlerType, item, Handler.Method));
      }
   }
}

Original Answer: You can use ObservableCollection<T>. This class has a CollectionChanged event in which you can subscribe/unsubscribe events as necessary.

I would create a base class (this is from memory, just to get the point accross).

public abstract class ManagedEventCollection<T> : IList<T>
{
   public ManagedEventCollection()
   {
      m_list = new ObservableCollection<T> ();
      m_list.CollectionChanged += CollectionChanged;
   }
   ... // Add/Remove/Indexer/Clear methods alter contents of m_list.
   protected abstract void OnItemAdded(T item);
   protected abstract void OnItemRemoved(T item);

   private ObservableCollection<T> m_list;
   private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs ea)
   {
      foreach (T item in ea.NewItems)
         OnItemAdded(item);
      foreach (T item in ea.OldItems)
         OnItemRemoved(item);
   }
}

Then, your derived type can do this:

public class DogManagedEventCollection : ManagedEventCollection<Dog>
{
   protected override OnItemAdded (Dog dog)
   {
      dog.Bark += Bark;
   }
   protected override OnItemRemoved (Dog dog)
   {
      dog.Bark -= Bark;
   }

   private void Bark(object sender, BarkEventArgs ea){...}
}

You could also subscribe with reflection if you really wanted, to, but that will be more error-prone and not as easy to read/maintain/understand.

like image 197
agent-j Avatar answered Oct 11 '22 11:10

agent-j