I have recently started writing C# from a strong JavaScript background, and found myself wanting to do something I do all the time in JavaScript: events.
Since I have a type system at my disposal, I figured I'd make use of it and make the event system type safe.
A quick note on existing options:
C#'s built-in event
library didn't really appeal to me - for one, I don't really like the syntax (eg listeners += callback
), but more than that, I wanted type-safety in the callbacks. (ie, I want the callback to be something like Action<T1, T2, ...>
, not public void delegate SomeEventHandler(object sender, EventArgs args)
).
Unity's built-in UnityEvent
looks pretty good, but these aren't collected and managed in any way. I decided to roll my own Events to go with my EventManager, but I think I could adapt it to use UnityEvent fairly easily.
The code compiles and works, but I am wondering if there's a more elegant solution - or even if there's just a way I could reduce the boilerplate in my own solution. (Generics seem to cause most if not all of the boilerplate, but I didn't see a better way to get type-safety in callbacks.)
Here's how it is used:
using EventSystem;
// create SomeEvent - uses a class stub, not sure how I feel about that
public class SomeEvent : BasicEvent<SomeEvent, SomeParameterType> { }
// listen to it somewhere else in the codebase...
EventManager.On<SomeEvent, SomeParameterType>((param) => /* ... */);
// and trigger it somewhere different still...
EventManager.Trigger<SomeEvent, SomeParameterType>(someParameter);
Here's how it is structured:
namespace EventSystem
EventManager.cs
, a static class that can be used to listen to / detach from and trigger eventsEventBehaviour.cs
, an abstract class that "registers" an event with theEventManager
using Unity'sAwake () { ... }
- basically glue between the event system and UnityIEventListener.cs
, generic interfaces that define a type-safe listener, and have extension methods for implementing listenerInvoke
methods for any of the defined genericsBasicListener.cs
, reusable implementations of each genericIEventListener
BasicEvent.cs
, reusable containers deriving fromEventBehaviour
for each genericBasicListener
And here are the files copy/pasted:
BasicEvent.cs
using System;
using System.Collections.Generic;
namespace EventSystem {
public class BasicEvent<E> : EventBehaviour where E : EventBehaviour {
public static BasicListener<E> listener = new BasicListener<E>();
override public IEventListenerBase Listener {
get {
return listener;
}
}
}
public class BasicEvent<E, P> : EventBehaviour where E : EventBehaviour {
public static BasicListener<E, P> listener = new BasicListener<E, P>();
override public IEventListenerBase Listener {
get {
return listener;
}
}
}
public class BasicEvent<E, P1, P2> : EventBehaviour where E : EventBehaviour {
public static BasicListener<E, P1, P2> listener = new BasicListener<E, P1, P2>();
override public IEventListenerBase Listener {
get {
return listener;
}
}
}
}
BasicListener.cs
using System;
using System.Collections.Generic;
namespace EventSystem {
public class BasicListener<E> : IEventListener where E: EventBehaviour {
private List<Action> _callbacks;
private Type _eventType;
public List<Action> Callbacks {
get {
return _callbacks;
}
}
public Type EventType {
get {
return _eventType;
}
}
public BasicListener () {
_eventType = typeof(E);
_callbacks = new List<Action>();
}
}
public class BasicListener<E, P> : IEventListener<P> where E: EventBehaviour {
private List<Action<P>> _callbacks;
private Type _eventType;
public List<Action<P>> Callbacks {
get {
return _callbacks;
}
}
public Type EventType {
get {
return _eventType;
}
}
public BasicListener () {
_eventType = typeof(E);
_callbacks = new List<Action<P>>();
}
}
public class BasicListener<E, P1, P2> : IEventListener<P1, P2> where E: EventBehaviour {
private List<Action<P1, P2>> _callbacks;
private Type _eventType;
public List<Action<P1, P2>> Callbacks {
get {
return _callbacks;
}
}
public Type EventType {
get {
return _eventType;
}
}
public BasicListener () {
_eventType = typeof(E);
_callbacks = new List<Action<P1, P2>>();
}
}
}
EventBehaviour.cs
using UnityEngine;
using System;
using System.Collections.Generic;
namespace EventSystem {
public abstract class EventBehaviour : MonoBehaviour {
abstract public IEventListenerBase Listener {
get;
}
void Awake () {
EventManager.Listeners.Add(this.Listener);
}
}
}
EventManager.cs
using System;
using System.Collections.Generic;
namespace EventSystem {
public static class EventManager {
public static List<IEventListenerBase> Listeners = new List<IEventListenerBase>();
private static IEventListener GetListenerFor<E> () where E : EventBehaviour {
Type eventType = typeof(E);
return Listeners.Find((listener) => listener.EventType == eventType) as IEventListener;
}
private static IEventListener<P> GetListenerFor<E, P> () where E : EventBehaviour {
List<IEventListener<P>> listeners;
listeners = Listeners.FindAll((listener) => listener is IEventListener<P>)
.ConvertAll<IEventListener<P>>((listener) => listener as IEventListener<P>);
return listeners.Find((listener) => listener.Callbacks is List<Action<P>>);
}
private static IEventListener<P1, P2> GetListenerFor<E, P1, P2> () where E : EventBehaviour {
List<IEventListener<P1, P2>> listeners;
listeners = Listeners.FindAll((listener) => listener is IEventListener<P1, P2>)
.ConvertAll<IEventListener<P1, P2>>((listener) => listener as IEventListener<P1, P2>);
return listeners.Find((listener) => listener.Callbacks is List<Action<P1, P2>>);
}
public static void On<E> (Action callback) where E : EventBehaviour {
IEventListener l = GetListenerFor<E>();
l.Callbacks.Add(callback);
}
public static void On<E, P> (Action<P> callback) where E : EventBehaviour {
IEventListener<P> l = GetListenerFor<E, P>();
l.Callbacks.Add(callback);
}
public static void On<E, P1, P2> (Action<P1, P2> callback) where E : EventBehaviour {
IEventListener<P1, P2> l = GetListenerFor<E, P1, P2>();
l.Callbacks.Add(callback);
}
public static bool Off<E> (Action callback) where E : EventBehaviour {
IEventListener l = GetListenerFor<E>();
return l.Callbacks.Remove(callback);
}
public static bool Off<E, P> (Action<P> callback) where E : EventBehaviour {
IEventListener<P> l = GetListenerFor<E, P>();
return l.Callbacks.Remove(callback);
}
public static bool Off<E, P1, P2> (Action<P1, P2> callback) where E : EventBehaviour {
IEventListener<P1, P2> l = GetListenerFor<E, P1, P2>();
return l.Callbacks.Remove(callback);
}
public static void Trigger<E> () where E : EventBehaviour {
IEventListener l = GetListenerFor<E>();
l.Invoke();
}
public static void Trigger<E, P> (P arg) where E : EventBehaviour {
IEventListener<P> l = GetListenerFor<E, P>();
l.Invoke<P>(arg);
}
public static void Trigger<E, P1, P2> (P1 arg1, P2 arg2) where E : EventBehaviour {
IEventListener<P1, P2> l = GetListenerFor<E, P1, P2>();
l.Invoke<P1, P2>(arg1, arg2);
}
}
}
IEventListener.cs
using System;
using System.Collections.Generic;
namespace EventSystem {
public interface IEventListenerBase {
Type EventType { get; }
}
public interface IEventListener : IEventListenerBase {
List<Action> Callbacks { get; }
}
public interface IEventListener<P1> : IEventListenerBase {
List<Action<P1>> Callbacks { get; }
}
public interface IEventListener<P1, P2> : IEventListenerBase {
List<Action<P1, P2>> Callbacks { get; }
}
public static class IEventListenerExtensions {
public static void Invoke (this IEventListener l) {
l.Callbacks.ForEach((cb) => cb());
}
public static void Invoke<P1> (this IEventListener<P1> l, P1 arg) {
l.Callbacks.ForEach((cb) => cb(arg));
}
public static void Invoke<P1, P2> (this IEventListener<P1, P2> l, P1 arg1, P2 arg2) {
l.Callbacks.ForEach((cb) => cb(arg1, arg2));
}
}
}