1
\$\begingroup\$

I decided that for some parts of my project, communication through events would be very handy, for that purpose I started writing the most basic part of the event system, an interface consisting of only 1 EventHandler with rather generic name:

public interface ICustomObservable<TArgs>
    where TArgs : EventArgs
{
    event EventHandler<TArgs> Notify;
}

It's being inherited by any class that wishes to notify someone about different events that are occurring.

It can be used obviously with any type that inherits EventArgs, but currently the most often used generic type argument is MediaEventArgs:

public class MediaEventArgs<TEnumeration> : EventArgs
    where TEnumeration : Enumeration<TEnumeration>
{
    public TEnumeration EventType { get; }
    public object AdditionalInfo { get; }

    public MediaEventArgs(TEnumeration eventType, object additionalInfo = null)
    {
        EventType = eventType ?? throw new ArgumentNullException(nameof(eventType), @"Enumeration cannot be null.");
        AdditionalInfo = additionalInfo;
    }
}

Please, refer to my recent self-answer for the implementation of Enumeration<> type.

For testing purposes the following derived classes can be created:

public class TestEnumeration1 : Enumeration<TestEnumeration1>
{
    public static TestEnumeration1 A = new TestEnumeration1(nameof(A), 0);
    public static TestEnumeration1 B = new TestEnumeration1(nameof(B), 1);

    protected TestEnumeration1(string name, int value)
        : base(name, value)
    {
    }
}

public class TestEnumeration2 : Enumeration<TestEnumeration2>
{
    public static TestEnumeration2 C = new TestEnumeration2(nameof(C), 0);
    public static TestEnumeration2 D = new TestEnumeration2(nameof(D), 1);

    protected TestEnumeration2(string name, int value)
        : base(name, value)
    {
    }
}

public class TestCustomObservable1 : ICustomObservable<MediaEventArgs<TestEnumeration1>>
{
    public event EventHandler<MediaEventArgs<TestEnumeration1>> Notify;

    public void TestNotification(TestEnumeration1 value)
    {
        Notify?.Invoke(this, new MediaEventArgs<TestEnumeration1>(value));
    }
}

public class TestCustomObservable2 : ICustomObservable<MediaEventArgs<TestEnumeration2>>
{
    public event EventHandler<MediaEventArgs<TestEnumeration2>> Notify;

    public void TestNotification(TestEnumeration2 value)
    {
        Notify?.Invoke(this, new MediaEventArgs<TestEnumeration2>(value));
    }
}

public class TestCustomObservable3 : ICustomObservable<MediaEventArgs<TestEnumeration1>>, ICustomObservable<MediaEventArgs<TestEnumeration2>>
{
    private event EventHandler<MediaEventArgs<TestEnumeration1>> _Notify;
    event EventHandler<MediaEventArgs<TestEnumeration1>> ICustomObservable<MediaEventArgs<TestEnumeration1>>.Notify
    {
        add => _Notify += value;
        remove => _Notify -= value;
    }

    public event EventHandler<MediaEventArgs<TestEnumeration2>> Notify;

    public void Test(TestEnumeration1 value)
    {
        _Notify?.Invoke(this, new MediaEventArgs<TestEnumeration1>(value));
    }

    public void Test2(TestEnumeration2 value)
    {
        Notify?.Invoke(this, new MediaEventArgs<TestEnumeration2>(value));
    }
}

Now some types require notifications from multiple sources for this case you can simply do:

Notifier1.Notify+=..
Notifier2.Notify+=..
Notifier3.Notify+=..

and of course somewhere down the code unsubscription is also required.

It works but it doesn't look nice and since most of the event handlers for the Notify event require an extra dictionary to test against each Enumeration type, we end up with long dictionary declarations, very repetitive and mostly 1 liner methods, I decide to introduce few helper classes to ease the job of the consumer.


Solving the long dictionary declarations problem is pretty easy. Few wrapper classes and it's basically done:

internal class ObservableMap<T, TArgs> : Dictionary<T, EventHandler<TArgs>>
    where TArgs : EventArgs
{
    protected Func<ObservableMap<T, TArgs>, TArgs, T> _invokator;

    internal ObservableMap(Func<ObservableMap<T, TArgs>, TArgs, T> invokator)
        : base()
    {
        _invokator = invokator;
    }

    internal ObservableMap(IDictionary<T, EventHandler<TArgs>> values)
        : base(values)
    {
    }

    internal virtual EventHandler<TArgs> GetHandler(TArgs args)
    {
        return this.TryGetValue(_invokator.Invoke(this, args), out var handler) ? handler: null;
    }
}

internal class MediaObservableMap<TEnumeration> : ObservableMap<TEnumeration, MediaEventArgs<TEnumeration>>
    where TEnumeration : Enumeration<TEnumeration>
{
    internal MediaObservableMap(
        Func<ObservableMap<TEnumeration, MediaEventArgs<TEnumeration>>,
            MediaEventArgs<TEnumeration>, TEnumeration> invokator)
        : base(invokator)
    {
    }

    internal MediaObservableMap(IDictionary<TEnumeration, EventHandler<MediaEventArgs<TEnumeration>>> values)
        : base(values)
    {
    }

    internal EventHandler<MediaEventArgs<TEnumeration>> GetHandler(TEnumeration enumerationValue)
    {
        return TryGetValue(enumerationValue, out var handler) ? handler : null;
    }
}

Example declarations:

var map1 = new ObservableMap<TestEnumeration1, MediaEventArgs<TestEnumeration1>>(
(map, args) => args.EventType)
{
    [TestEnumeration1.A] = (sender, args) => Item("map1 - A", sender, args),
};

var map2 = new MediaObservableMap<TestEnumeration2>((map, args) => args.EventType)
{
    [TestEnumeration2.C] = (sender, args) => Item("map2 - C", sender, args),
    [TestEnumeration2.D] = (sender, args) => Item("map2 - D", sender, args),
};

And the second problem is solved using the help of the MultipleProvidersCache class:

internal class MultipleProvidersCache
{
    protected readonly ProviderActionCache<EventHandler<object>> _notificationsCache =
        new ProviderActionCache<EventHandler<object>>();

    protected readonly ProviderActionCache<Action> _providerUnsubscriberCache = new ProviderActionCache<Action>();

    internal virtual void AddProvider<T, TArgs>(ICustomObservable<TArgs> provider, ObservableMap<T, TArgs> map)
        where TArgs : EventArgs
    {
        var providerType = provider.GetType();
        _notificationsCache.Add<TArgs>(providerType, (sender, args) =>
        {
            var unboxedArgs = (TArgs) args;
            map.GetHandler(unboxedArgs)?.Invoke(sender, unboxedArgs);
        });
        void NotifierDelegate(object sender, TArgs args) => Provider_OnNotify(sender, args, providerType);
        provider.Notify += NotifierDelegate;
        _providerUnsubscriberCache.Add<TArgs>(providerType, () => provider.Notify -= NotifierDelegate);
    }

    internal virtual bool RemoveProvider<TProvider, TArgs>()
        where TArgs : EventArgs
        where TProvider : ICustomObservable<TArgs>
    {
        return RemoveProviderImpl<TArgs>(typeof(TProvider));
    }

    protected virtual bool RemoveProviderImpl<TArgs>(Type providerType)
        where TArgs : EventArgs
    {
        if (_notificationsCache.RemoveAction<TArgs>(providerType))
        {
            _providerUnsubscriberCache.GetAction<TArgs>(providerType).Invoke();
            _providerUnsubscriberCache.RemoveAction<TArgs>(providerType);
            return true;
        }
        return false;
    }

    protected virtual void Provider_OnNotify<TArgs>(object sender, TArgs e, Type providerType)
        where TArgs : EventArgs
    {
        if (_notificationsCache.TryGetAction<TArgs>(providerType, out var action))
        {
            action.Invoke(sender, e);
        }
    }

    protected sealed class ProviderActionCache<TAction> : IEnumerable<KeyValuePair<Type, Dictionary<Type, TAction>>>
    {
        private readonly IDictionary<Type, Dictionary<Type, TAction>> _dictionary;

        internal ProviderActionCache()
        {
            _dictionary = new Dictionary<Type, Dictionary<Type, TAction>>();
        }

        internal void Add<TArgs>(Type key, TAction value)
            where TArgs : EventArgs
        {
            if (_dictionary.TryGetValue(key, out var values))
            {
                values.Add(typeof(TArgs), value);
            }
            else
            {
                _dictionary.Add(key, new Dictionary<Type, TAction> {[typeof(TArgs)] = value});
            }
        }

        internal bool RemoveProvider(Type providerType)
        {
            return _dictionary.Remove(providerType);
        }

        internal bool RemoveAction<TArgs>(Type providerType)
            where TArgs : EventArgs
        {
            return _dictionary.TryGetValue(providerType, out var values) && values.Remove(typeof(TArgs));
        }

        internal bool TryGetProvider(Type providerType, out Dictionary<Type, TAction> values)
        {
            return _dictionary.TryGetValue(providerType, out values);
        }

        internal bool TryGetAction<TArgs>(Type providerType, out TAction action)
            where TArgs : EventArgs
        {
            if (TryGetProvider(providerType, out var value))
            {
                action = value[typeof(TArgs)];
                return true;
            }
            action = default(TAction);
            return false;
        }

        internal TAction GetAction<TArgs>(Type providerType)
            where TArgs : EventArgs
        {
            return _dictionary[providerType][typeof(TArgs)];
        }

        internal Dictionary<Type, TAction> GetProvider(Type providerType)
        {
            return _dictionary[providerType];
        }

        #region Implementation of IEnumerable

        public IEnumerator<KeyValuePair<Type, Dictionary<Type, TAction>>> GetEnumerator()
        {
            return _dictionary.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion
    }
}

For the cases when a class implements ICustomObservable<> only 1 time, it's pretty straight forward, but if a class implements the interface multiple times, MultipleProvidersCache would internally treat it as if it were multiple separate types all unified under the same class type, which means you cannot have multiple ObservableMap<>s on the same provider.

The following should crash:

TestCustomObservable3 tco3 = new TestCustomObservable3();
MultipleProvidersCache mpc = new MultipleProvidersCache();
mpc.AddProvider(tco3, map1);
mpc.AddProvider(tco3, map1);

But not this:

TestCustomObservable3 tco3 = new TestCustomObservable3();
MultipleProvidersCache mpc = new MultipleProvidersCache();
mpc.AddProvider(tco3, map1);
mpc.AddProvider(tco3, map2);

Example usage

MultipleProvidersCache mpc = new MultipleProvidersCache();
TestCustomObservable1 tco1 = new TestCustomObservable1();
TestCustomObservable2 tco2 = new TestCustomObservable2();
TestCustomObservable3 tco3 = new TestCustomObservable3();

var map1 = new ObservableMap<TestEnumeration1, MediaEventArgs<TestEnumeration1>>(
(map, args) => args.EventType)
{
    [TestEnumeration1.A] = (sender, args) => Item("map1 - A", sender, args),
};

var map2 = new MediaObservableMap<TestEnumeration2>((map, args) => args.EventType)
{
    [TestEnumeration2.C] = (sender, args) => Item("map2 - C", sender, args),
    [TestEnumeration2.D] = (sender, args) => Item("map2 - D", sender, args),
};

mpc.AddProvider(tco1, map1);
mpc.AddProvider(tco2, map2);
mpc.AddProvider(tco3, map2);
mpc.AddProvider(tco3, map1);

tco1.TestNotification(TestEnumeration1.A);
tco2.TestNotification(TestEnumeration2.D);
mpc.RemoveProvider<TestCustomObservable2, MediaEventArgs<TestEnumeration2>>();
tco1.TestNotification(TestEnumeration1.A);
tco1.TestNotification(TestEnumeration1.B);
tco2.TestNotification(TestEnumeration2.C);
tco3.Test(TestEnumeration1.B);
tco3.Test(TestEnumeration1.A);
mpc.RemoveProvider<TestCustomObservable3, MediaEventArgs<TestEnumeration1>>();
tco3.Test2(TestEnumeration2.D);
tco3.Test(TestEnumeration1.A);

private static void Item<TEnumeration>(string value, object sender, MediaEventArgs<TEnumeration> args)
    where TEnumeration : Enumeration<TEnumeration>
{
    Console.WriteLine($"{value}{Environment.NewLine}" +
                        $"Sender = {sender}{Environment.NewLine}" +
                        $"EventType = {args.EventType}");
}

This should produce the following output:

map1 - A
Sender = CodeReviewCSharp7.TestCustomObservable1
EventType = A
map2 - D
Sender = CodeReviewCSharp7.TestCustomObservable2
EventType = D
map1 - A
Sender = CodeReviewCSharp7.TestCustomObservable1
EventType = A
map1 - A
Sender = CodeReviewCSharp7.TestCustomObservable3
EventType = A
map2 - D
Sender = CodeReviewCSharp7.TestCustomObservable3
EventType = D

Any critique or suggestions are welcome.

\$\endgroup\$
  • 1
    \$\begingroup\$ Feeling like reinventing the observer pattern? ;-) \$\endgroup\$ – t3chb0t Apr 4 '18 at 6:21
3
\$\begingroup\$

Maybe I misunderstood your intent, but MultipleProvidersCache looks a lot like an overly complex (API-wise) event aggregator (aka message hub) implementation. Even though different implementations have different flavors, normally when writing one, you should try to let go of event keyword. For example, the most basic API can look like this:

interface IEventAggregator
{
    IDisposable Subsribe<TMessage>(Action<TMessage> action);
    void Publish<TMessage>(TMessage message);
}

Here TMessage acts as EventArgs replacement. The idea is that any object can subscribe to "events" by calling Subsribe method and any object can invoke "events" by calling Publish. IEventAggregator acts as a mediator and passes messages from publishers to the list of subscribed Actions.

P.S. Note, that some commonly used frameworks (e.g. Prism, MvvmLight, and most other MVVM based frameworks) got you covered and already have some messaging system in place. Nuget has multiple standalone implementations available as well, if you are working on, say, Unity project. However implementing your own event aggregator is a great exercise (I have actually shared my implementation on SR, when I was playing around with Dataflow).

\$\endgroup\$
  • \$\begingroup\$ This looks very familiar... almost like the Subject from the reactive extensions that combines both the IObserver and the IObservable interfaces. \$\endgroup\$ – t3chb0t Apr 4 '18 at 8:23
  • \$\begingroup\$ The point is that whenever Notifier X fires his Notify event, if X has already been mapped in MultipleProvidersCache along with an ObservableMap, only the map associated with this specific X notifier should know that an event has occurred. API-wise the methods are being invoked in fairly simple way. I have used a very similar interface to this one except that I still had EventArgs there, in a different situation where I don't mind sending notifications to all subscribers, instead of just a selected few(1). \$\endgroup\$ – Denis Apr 4 '18 at 14:19
  • \$\begingroup\$ @Denis the idea is, that you always publish a strongly typed message. So every message is only sent to subscribers that can handle message of that type. If you want to send a notifications to some subscribers but not to other subscribers, you just need to create two messages of different type. \$\endgroup\$ – Nikita B Apr 4 '18 at 14:36
  • \$\begingroup\$ @Denis with this approach you can easily "find usages" of any given message to quickly lookup both publishers and subscribers. And that's where I feel like your implementation comes short. Looking at your code it is really hard to figure out, which class handles which event under which circumstances. \$\endgroup\$ – Nikita B Apr 4 '18 at 14:39
  • \$\begingroup\$ I think it will be almost the same thing, the reason the current lookup looks complicated at first glance, is because you can have different providers that use the same TMessage in which case, I dont want every notifier's map that uses TMessage type to get invoked but only the one that the user requested at the start with AddProvider(provider1, map1) , I think you also have that problem, which to me seems like it wouldn't require a lot less work. If you have better idea of how this will work, can you please provide an example usage? :) \$\endgroup\$ – Denis Apr 4 '18 at 14:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.