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.