// =============================================================================== // TinyIoC - TinyMessenger // // A simple messenger/event aggregator. // // https://github.com/grumpydev/TinyIoC/blob/master/src/TinyIoC/TinyMessenger.cs // =============================================================================== // Copyright © Steven Robbins. All rights reserved. // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY // OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT // LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND // FITNESS FOR A PARTICULAR PURPOSE. // =============================================================================== namespace Swan.Messaging { using System.Threading.Tasks; using System; using System.Collections.Generic; using System.Linq; #region Message Types / Interfaces /// /// Message proxy definition. /// /// A message proxy can be used to intercept/alter messages and/or /// marshal delivery actions onto a particular thread. /// public interface IMessageHubProxy { /// /// Delivers the specified message. /// /// The message. /// The subscription. void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription); } /// /// Default "pass through" proxy. /// /// Does nothing other than deliver the message. /// public sealed class MessageHubDefaultProxy : IMessageHubProxy { private MessageHubDefaultProxy() { // placeholder } /// /// Singleton instance of the proxy. /// public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy(); /// /// Delivers the specified message. /// /// The message. /// The subscription. public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription) => subscription.Deliver(message); } #endregion #region Hub Interface /// /// Messenger hub responsible for taking subscriptions/publications and delivering of messages. /// public interface IMessageHub { /// /// Subscribe to a message type with the given destination and delivery action. /// Messages will be delivered via the specified proxy. /// /// All messages of this type will be delivered. /// /// Type of message. /// Action to invoke when message is delivered. /// Use strong references to destination and deliveryAction. /// Proxy to use when delivering the messages. /// MessageSubscription used to unsubscribing. MessageHubSubscriptionToken Subscribe( Action deliveryAction, bool useStrongReferences, IMessageHubProxy proxy) where TMessage : class, IMessageHubMessage; /// /// Subscribe to a message type with the given destination and delivery action with the given filter. /// Messages will be delivered via the specified proxy. /// All references are held with WeakReferences /// Only messages that "pass" the filter will be delivered. /// /// Type of message. /// Action to invoke when message is delivered. /// The message filter. /// Use strong references to destination and deliveryAction. /// Proxy to use when delivering the messages. /// /// MessageSubscription used to unsubscribing. /// MessageHubSubscriptionToken Subscribe( Action deliveryAction, Func messageFilter, bool useStrongReferences, IMessageHubProxy proxy) where TMessage : class, IMessageHubMessage; /// /// Unsubscribe from a particular message type. /// /// Does not throw an exception if the subscription is not found. /// /// Type of message. /// Subscription token received from Subscribe. void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) where TMessage : class, IMessageHubMessage; /// /// Publish a message to any subscribers. /// /// Type of message. /// Message to deliver. void Publish(TMessage message) where TMessage : class, IMessageHubMessage; /// /// Publish a message to any subscribers asynchronously. /// /// Type of message. /// Message to deliver. /// A task from Publish action. Task PublishAsync(TMessage message) where TMessage : class, IMessageHubMessage; } #endregion #region Hub Implementation /// /// /// The following code describes how to use a MessageHub. Both the /// subscription and the message sending are done in the same place but this is only for explanatory purposes. /// /// class Example /// { /// using Swan; /// using Swan.Components; /// /// static void Main() /// { /// // using DependencyContainer to create an instance of MessageHub /// var messageHub = DependencyContainer /// .Current /// .Resolve<IMessageHub>() as MessageHub; /// /// // create an instance of the publisher class /// // which has a string as its content /// var message = new MessageHubGenericMessage<string>(new object(), "SWAN"); /// /// // subscribe to the publisher's event /// // and just print out the content which is a string /// // a token is returned which can be used to unsubscribe later on /// var token = messageHub /// .Subscribe<MessageHubGenericMessage<string>>(m => m.Content.Info()); /// /// // publish the message described above which is /// // the string 'SWAN' /// messageHub.Publish(message); /// /// // unsuscribe, we will no longer receive any messages /// messageHub.Unsubscribe<MessageHubGenericMessage<string>>(token); /// /// Terminal.Flush(); /// } /// /// } /// /// public sealed class MessageHub : IMessageHub { #region Private Types and Interfaces private readonly object _subscriptionsPadlock = new object(); private readonly Dictionary> _subscriptions = new Dictionary>(); private class WeakMessageSubscription : IMessageHubSubscription where TMessage : class, IMessageHubMessage { private readonly WeakReference _deliveryAction; private readonly WeakReference _messageFilter; /// /// Initializes a new instance of the class. /// /// The subscription token. /// The delivery action. /// The message filter. /// subscriptionToken /// or /// deliveryAction /// or /// messageFilter. public WeakMessageSubscription( MessageHubSubscriptionToken subscriptionToken, Action deliveryAction, Func messageFilter) { SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken)); _deliveryAction = new WeakReference(deliveryAction); _messageFilter = new WeakReference(messageFilter); } public MessageHubSubscriptionToken SubscriptionToken { get; } public bool ShouldAttemptDelivery(IMessageHubMessage message) { return _deliveryAction.IsAlive && _messageFilter.IsAlive && ((Func) _messageFilter.Target).Invoke((TMessage) message); } public void Deliver(IMessageHubMessage message) { if (_deliveryAction.IsAlive) { ((Action) _deliveryAction.Target).Invoke((TMessage) message); } } } private class StrongMessageSubscription : IMessageHubSubscription where TMessage : class, IMessageHubMessage { private readonly Action _deliveryAction; private readonly Func _messageFilter; /// /// Initializes a new instance of the class. /// /// The subscription token. /// The delivery action. /// The message filter. /// subscriptionToken /// or /// deliveryAction /// or /// messageFilter. public StrongMessageSubscription( MessageHubSubscriptionToken subscriptionToken, Action deliveryAction, Func messageFilter) { SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken)); _deliveryAction = deliveryAction; _messageFilter = messageFilter; } public MessageHubSubscriptionToken SubscriptionToken { get; } public bool ShouldAttemptDelivery(IMessageHubMessage message) => _messageFilter.Invoke((TMessage) message); public void Deliver(IMessageHubMessage message) => _deliveryAction.Invoke((TMessage) message); } #endregion #region Subscription dictionary private class SubscriptionItem { public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription) { Proxy = proxy; Subscription = subscription; } public IMessageHubProxy Proxy { get; } public IMessageHubSubscription Subscription { get; } } #endregion #region Public API /// /// Subscribe to a message type with the given destination and delivery action. /// Messages will be delivered via the specified proxy. /// /// All messages of this type will be delivered. /// /// Type of message. /// Action to invoke when message is delivered. /// Use strong references to destination and deliveryAction. /// Proxy to use when delivering the messages. /// MessageSubscription used to unsubscribing. public MessageHubSubscriptionToken Subscribe( Action deliveryAction, bool useStrongReferences = true, IMessageHubProxy? proxy = null) where TMessage : class, IMessageHubMessage { return Subscribe(deliveryAction, m => true, useStrongReferences, proxy); } /// /// Subscribe to a message type with the given destination and delivery action with the given filter. /// Messages will be delivered via the specified proxy. /// All references are held with WeakReferences /// Only messages that "pass" the filter will be delivered. /// /// Type of message. /// Action to invoke when message is delivered. /// The message filter. /// Use strong references to destination and deliveryAction. /// Proxy to use when delivering the messages. /// /// MessageSubscription used to unsubscribing. /// public MessageHubSubscriptionToken Subscribe( Action deliveryAction, Func messageFilter, bool useStrongReferences = true, IMessageHubProxy? proxy = null) where TMessage : class, IMessageHubMessage { if (deliveryAction == null) throw new ArgumentNullException(nameof(deliveryAction)); if (messageFilter == null) throw new ArgumentNullException(nameof(messageFilter)); lock (_subscriptionsPadlock) { if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) { currentSubscriptions = new List(); _subscriptions[typeof(TMessage)] = currentSubscriptions; } var subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage)); IMessageHubSubscription subscription; if (useStrongReferences) { subscription = new StrongMessageSubscription( subscriptionToken, deliveryAction, messageFilter); } else { subscription = new WeakMessageSubscription( subscriptionToken, deliveryAction, messageFilter); } currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription)); return subscriptionToken; } } /// public void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) where TMessage : class, IMessageHubMessage { if (subscriptionToken == null) throw new ArgumentNullException(nameof(subscriptionToken)); lock (_subscriptionsPadlock) { if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) return; var currentlySubscribed = currentSubscriptions .Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken)) .ToList(); currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub)); } } /// /// Publish a message to any subscribers. /// /// Type of message. /// Message to deliver. public void Publish(TMessage message) where TMessage : class, IMessageHubMessage { if (message == null) throw new ArgumentNullException(nameof(message)); List currentlySubscribed; lock (_subscriptionsPadlock) { if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) return; currentlySubscribed = currentSubscriptions .Where(sub => sub.Subscription.ShouldAttemptDelivery(message)) .ToList(); } currentlySubscribed.ForEach(sub => { try { sub.Proxy.Deliver(message, sub.Subscription); } catch { // Ignore any errors and carry on } }); } /// /// Publish a message to any subscribers asynchronously. /// /// Type of message. /// Message to deliver. /// A task with the publish. public Task PublishAsync(TMessage message) where TMessage : class, IMessageHubMessage { return Task.Run(() => Publish(message)); } #endregion } #endregion }