Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Register event handler for specific subclass

Ok, code structure question:

Let's say I have a class, FruitManager, that periodically receives Fruit objects from some data-source. I also have some other classes that need to get notified when these Fruit objects are received. However, each class is only interested in certain types of fruit, and each fruit has different logic for how it should be handled. Say for example the CitrusLogic class has methods OnFruitReceived(Orange o) and OnFruitReceived(Lemon l), which should be called when the respective subtype of fruit is received, but it doesn't need to be notified of other fruits.

Is there a way to elegantly handle this in C# (presumably with events or delegates)? Obviously I could just add generic OnFruitReceived(Fruit f) event handlers, and use if statements to filter unwanted subclasses, but this seems inelegant. Does anyone have a better idea? Thanks!

Edit: I just found generic delegates and they seem like they could be a good solution. Does that sound like a good direction to go?

like image 718
thomas88wp Avatar asked May 21 '15 15:05

thomas88wp


3 Answers

First off, Unity supports a subset of .NET 3.5 where the particular subset depends on your build parameters.

Moving on to your question, the general event pattern in C# is to use delegates and the event keyword. Since you want handlers only to be called if the incoming fruit is compatible with its method definition, you can use a dictionary to accomplish the lookup. The trick is what type to store the delegates as. You can use a little type magic to make it work and store everything as

Dictionary<Type, Action<Fruit>> handlers = new Dictionary<Type, Action<Fruit>>();

This is not ideal, because now all the handlers seem to accept Fruit instead of the more specific types. This is only the internal representation however, publicly people will still add specific handlers via

public void RegisterHandler<T>(Action<T> handler) where T : Fruit

This keeps the public API clean and type specific. Internally the delegate needs to change from Action<T> to Action<Fruit>. To do this create a new delegate that takes in a Fruit and transforms it into a T.

Action<Fruit> wrapper = fruit => handler(fruit as T);

This is of course not a safe cast. It will crash if it is passed anything that is not T (or inherits from T). That is why it is very important it is only stored internally and not exposed outside the class. Store this function under the Type key typeof(T) in the handlers dictionary.

Next to invoke the event requires a custom function. This function needs to invoke all the event handlers from the type of the argument all the way up the inheritance chain to the most generic Fruit handlers. This allows a function to be trigger on any subtype arguments as well, not just its specific type. This seems the intuitive behavior to me, but can be left out if desired.

Finally, a normal event can be exposed to allow catch-all Fruit handlers to be added in the usual way.

Below is the full example. Note that the example is fairly minimal and excludes some typical safety checks such as null checking. There is also a potential infinite loop if there is no chain of inheritance from child to parent. An actual implementation should be expanded as seen fit. It could also use a few optimizations. Particularly in high use scenarios caching the inheritance chains could be important.

public class Fruit { }

class FruitHandlers
{
    private Dictionary<Type, Action<Fruit>> handlers = new Dictionary<Type, Action<Fruit>>();

    public event Action<Fruit> FruitAdded
    {
        add
        {
            handlers[typeof(Fruit)] += value;
        }
        remove
        {
            handlers[typeof(Fruit)] -= value;
        }
    }

    public FruitHandlers()
    {
        handlers = new Dictionary<Type, Action<Fruit>>();
        handlers.Add(typeof(Fruit), null);
    }

    static IEnumerable<Type> GetInheritanceChain(Type child, Type parent)
    {
        for (Type type = child; type != parent; type = type.BaseType)
        {
            yield return type;
        }
        yield return parent;
    }

    public void RegisterHandler<T>(Action<T> handler) where T : Fruit
    {
        Type type = typeof(T);
        Action<Fruit> wrapper = fruit => handler(fruit as T);

        if (handlers.ContainsKey(type))
        {
            handlers[type] += wrapper;
        }
        else
        {
            handlers.Add(type, wrapper);
        }
    }

    private void InvokeFruitAdded(Fruit fruit)
    {
        foreach (var type in GetInheritanceChain(fruit.GetType(), typeof(Fruit)))
        {
            if (handlers.ContainsKey(type) && handlers[type] != null)
            {
                handlers[type].Invoke(fruit);
            }
        }
    }
}
like image 198
C. Potts Avatar answered Oct 30 '22 17:10

C. Potts


This sounds like a problem for the Observer pattern. Using System.Reactive.Linq, we also get access to the Observable class which contains a series of Linq methods for observers, including .OfType<>

fruitSource.OfType<CitrusFruit>.Subscribe(new CitrusLogic());
fruitSource.OfType<LemonFruit>.Subscribe(new LemonLogic());

...
public class Ciruslogic : IObersver<CitrusFruit>
{ ... }

If you need to add all existing overloads by type, such as all implementations of AFruitLogic<TFruit>, you'll need to scan the assembly using reflection or look into various IoC methodologies such as MEF

like image 22
David Avatar answered Oct 30 '22 15:10

David


I've been using a general purpose event aggregator which could help you here.

Following code isn't written in .Net2.0 but you can easily modify it to be compatible with .Net2.0 by eliminating use of few Linq methods.

namespace Eventing
{
    public class EventAggregator : IEventAggregator
    {
        private readonly Dictionary<Type, List<WeakReference>> eventSubscriberLists =
            new Dictionary<Type, List<WeakReference>>();
        private readonly object padLock = new object();

        public void Subscribe(object subscriber)
        {
            Type type = subscriber.GetType();
            var subscriberTypes = GetSubscriberInterfaces(type)
                .ToArray();
            if (!subscriberTypes.Any())
            {
                throw new ArgumentException("subscriber doesn't implement ISubscriber<>");
            }

            lock (padLock)
            {
                var weakReference = new WeakReference(subscriber);
                foreach (var subscriberType in subscriberTypes)
                {
                    var subscribers = GetSubscribers(subscriberType);
                    subscribers.Add(weakReference);
                }
            }
        }

        public void Unsubscribe(object subscriber)
        {
            Type type = subscriber.GetType();
            var subscriberTypes = GetSubscriberInterfaces(type);

            lock (padLock)
            {
                foreach (var subscriberType in subscriberTypes)
                {
                    var subscribers = GetSubscribers(subscriberType);
                    subscribers.RemoveAll(x => x.IsAlive && object.ReferenceEquals(x.Target, subscriber));
                }
            }
        }

        public void Publish<TEvent>(TEvent eventToPublish)
        {
            var subscriberType = typeof(ISubscriber<>).MakeGenericType(typeof(TEvent));
            var subscribers = GetSubscribers(subscriberType);
            List<WeakReference> subscribersToRemove = new List<WeakReference>();

            WeakReference[] subscribersArray;
            lock (padLock)
            {
                subscribersArray = subscribers.ToArray();
            }

            foreach (var weakSubscriber in subscribersArray)
            {
                ISubscriber<TEvent> subscriber = (ISubscriber<TEvent>)weakSubscriber.Target;
                if (subscriber != null)
                {
                    subscriber.OnEvent(eventToPublish);
                }
                else
                {
                    subscribersToRemove.Add(weakSubscriber);
                }
            }
            if (subscribersToRemove.Any())
            {
                lock (padLock)
                {
                    foreach (var remove in subscribersToRemove)
                        subscribers.Remove(remove);
                }
            }
        }

        private List<WeakReference> GetSubscribers(Type subscriberType)
        {
            List<WeakReference> subscribers;
            lock (padLock)
            {
                var found = eventSubscriberLists.TryGetValue(subscriberType, out subscribers);
                if (!found)
                {
                    subscribers = new List<WeakReference>();
                    eventSubscriberLists.Add(subscriberType, subscribers);
                }
            }
            return subscribers;
        }

        private IEnumerable<Type> GetSubscriberInterfaces(Type subscriberType)
        {
            return subscriberType
                .GetInterfaces()
                .Where(i => i.IsGenericType &&
                    i.GetGenericTypeDefinition() == typeof(ISubscriber<>));
        }
    }

    public interface IEventAggregator
    {
        void Subscribe(object subscriber);
        void Unsubscribe(object subscriber);
        void Publish<TEvent>(TEvent eventToPublish);
    }

    public interface ISubscriber<in T>
    {
        void OnEvent(T e);
    }
}

Your models or whatever you want to publish

public class Fruit
{

}

class Orange : Fruit
{
}

class Apple : Fruit
{
}

class Lemon : Fruit
{
}

//Class which handles citrus events
class CitrusLogic : ISubscriber<Orange>, ISubscriber<Lemon>
{
    void ISubscriber<Orange>.OnEvent(Orange e)
    {
        Console.WriteLine(string.Format("Orange event fired: From {0}", this.GetType().Name));
    }

    void ISubscriber<Lemon>.OnEvent(Lemon e)
    {
        Console.WriteLine(string.Format("Lemon event fired: From {0}", this.GetType().Name));
    }
}

//Class which handles Apple events
class AppleLogic : ISubscriber<Apple>
{
    void ISubscriber<Apple>.OnEvent(Apple e)
    {
        Console.WriteLine(string.Format("Apple event fired: From {0}", this.GetType().Name));
    }
}

Then use it as follows

void Main()
{
    EventAggregator aggregator = new EventAggregator();

    CitrusLogic cl =new CitrusLogic();
    AppleLogic al =new AppleLogic();
    aggregator.Subscribe(cl);
    aggregator.Subscribe(al);
    //...

    aggregator.Publish(new Apple());
    aggregator.Publish(new Lemon());
    aggregator.Publish(new Orange());
}

Which outputs

Apple event fired: From AppleLogic
Lemon event fired: From CitrusLogic
Orange event fired: From CitrusLogic

Note: The version of event aggregator provided above uses Weak event pattern, so you must need a strong reference to the subscribers to keep it alive. If you want it to be strong reference, you can simply convert the weak reference to strong reference.

like image 44
Sriram Sakthivel Avatar answered Oct 30 '22 15:10

Sriram Sakthivel