Reputation: 11828
Here is my code:
static class MessageHandler<T> where T : Message
{
public delegate void MessageDelegate(T m);
private static MessageDelegate messageHandlers;
public static void Publish(T message)
{
messageHandlers(message);
}
public static void Subscribe(MessageDelegate messageHandler)
{
messageHandlers += messageHandler;
}
}
Message
is just an empty class that other classes can inherit from. Most Message
derivations are simple objects with relevant properties (this is for a game so there might be a PlayerDamagedMessage
with the damage and assailant as the properties.)
Basically, in the Player
class when they are attacked they would publish a PlayerDamagedMessage
and then other classes that want to know about that can subscribe and then receive the relevant details when it occurs.
The reason why this works with more than one message is because the way generic classes work in C#. By this I mean that there will be a copy of the delegate for each different generic type used under the covers.
I actually figured this out by accident while playing around and am excited because it really simplified my code and looks almost like a design pattern at this point.
I'm posting here to ask about the potential downsides for using this kind of approach. Is there a limit to the amount of generic delegates under the covers? How well would something like this scale?
Also, is there any way to have some sort of generic type-inference so that the statements don't have to be this long?
MessageHandler<MessagePlayerDamaged>.Publish(new MessagePlayerDamaged(this, this));
Upvotes: 4
Views: 2054
Reputation: 741
I found a way to do it using generics, I added a private Registry class which stores the static delegate:
class Message
{
}
class MessageHandler
{
public static void Publish<T>(T message) where T : Message
{
Registry<T>.action(message);
}
public static void Subscribe<T>(Action<T> h) where T : Message
{
Registry<T>.action += h;
}
private class Registry<T> where T : Message
{
public static Action<T> action;
}
}
This way you don't have to add the type arguments:
class IntMessage : Message
{
public int Value = 100;
}
class StringMessage : Message
{
public string Value = "a string";
}
static void Main(string[] args)
{
MessageHandler.Subscribe((StringMessage m) => Console.WriteLine("String : " + m.Value));
MessageHandler.Subscribe((StringMessage m) => Console.WriteLine("2nd String : " + m.Value));
MessageHandler.Subscribe((IntMessage m) => Console.WriteLine("Int : " + m.Value));
MessageHandler.Subscribe((IntMessage m) => Console.WriteLine("2nd Int : " + m.Value));
MessageHandler.Publish(new IntMessage());
MessageHandler.Publish(new StringMessage());
}
Upvotes: 0
Reputation: 25856
There is a solution I use in my projects.
public class MessageDispatcher {
private readonly Dictionary<Type, MulticastDelegate> registeredHandlers = new Dictionary<Type, MulticastDelegate>();
private delegate void MessageActionDelegate<in T>(T message);
public void Register<T>(Action<T> action) {
Type messageType = typeof (T);
if (registeredHandlers.ContainsKey(messageType)) {
var messageDelegate = (MessageActionDelegate<T>) registeredHandlers[messageType];
registeredHandlers[messageType] = messageDelegate + new MessageActionDelegate<T>(action);
}
else {
registeredHandlers.Add(messageType, new MessageActionDelegate<T>(action));
}
}
public void Deregister<T>() {
Type messageType = typeof (T);
if (registeredHandlers.ContainsKey(messageType)) {
registeredHandlers.Remove(messageType);
}
}
public void DeregisterAll() {
registeredHandlers.Clear();
}
public void Send<T>(T message) {
Type messageType = typeof (T);
if (!registeredHandlers.ContainsKey(messageType)) return;
((MessageActionDelegate<T>) registeredHandlers[messageType])(message);
}
}
And test example:
private static void Main(string[] args) {
var messenger = new MessageDispatcher();
messenger.Register<Message>(m => Console.WriteLine(m.Text));
messenger.Send(new Message() { Text = "Good morning, sir."});
messenger.Register<Message>(m => Console.WriteLine(m.Text + " It's nice weather today."));
messenger.Register<Notification>(n => Console.WriteLine(n.Text));
messenger.Send(new Message() { Text = "How do you feel? "});
messenger.Send(new Notification() { Text = "Cup of tea, sir?" });
messenger.Deregister<Message>();
messenger.Send(new Message() { Text = "Good bye" });
Console.ReadLine();
}
public class Message {
public string Text { get; set; }
}
public class Notification {
public string Text { get; set; }
}
You can make MessageDispatcher
a singleton.
And if your application is multi-threaded then you need to think about thread-safety.
Upvotes: 1
Reputation: 1309
The main differences to C# events:
player1
and not player2
Upvotes: 1
Reputation: 6401
I've actually used a very similar pattern with much success. One further step I took was encapsulating the actual message handlers within a MessageHandlerRegistry
to allow for cleaner syntax. Here is your example modified:
Message.cs
public class Message
{
}
MessageHandler.cs
public class MessageHandler<T> where T : Message
{
private Action<T> messageHandlers;
public void Publish(T message)
{
messageHandlers(message);
}
public void Subscribe(Action<T> messageHandler)
{
messageHandlers = (Action<T>) Delegate.Combine(messageHandlers, messageHandler);
}
}
MessageHandlerRegistry.cs
public static class MessageHandlerRegistry
{
private static readonly IDictionary<Type, object> _handlers = new Dictionary<Type, object>();
public static void Publish<T>(T m) where T : Message
{
if (_handlers.ContainsKey(typeof (T)))
{
((MessageHandler<T>) _handlers[typeof (T)]).Publish(m);
}
}
public static void Subscribe<T>(Action<T> messageHandler) where T : Message
{
if (!_handlers.ContainsKey(typeof (T)))
{
_handlers[typeof (T)] = new MessageHandler<T>();
}
((MessageHandler<T>) _handlers[typeof (T)]).Subscribe(messageHandler);
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
MessageHandlerRegistry.Subscribe((Message m) => Console.WriteLine("Message received."));
MessageHandlerRegistry.Publish(new Message());
}
}
The only downside I have seen to this is being overly-loosly coupled, and in some situations where it would make more sense to use a traditional event based approach, sometimes it is easier to just publish a message.
Upvotes: 1