Search code examples
c#.netevents.net-2.0

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?


Solution

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