using System; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace Xamarin.Forms { public interface IMessagingCenter { void Send(TSender sender, string message, TArgs args) where TSender : class; void Send(TSender sender, string message) where TSender : class; void Subscribe(object subscriber, string message, Action callback, TSender source = null) where TSender : class; void Subscribe(object subscriber, string message, Action callback, TSender source = null) where TSender : class; void Unsubscribe(object subscriber, string message) where TSender : class; void Unsubscribe(object subscriber, string message) where TSender : class; } public class MessagingCenter : IMessagingCenter { public static IMessagingCenter Instance { get; } = new MessagingCenter(); class Sender : Tuple { public Sender(string message, Type senderType, Type argType) : base(message, senderType, argType) { } } delegate bool Filter(object sender); class MaybeWeakReference { WeakReference DelegateWeakReference { get; } object DelegateStrongReference { get; } readonly bool _isStrongReference; public MaybeWeakReference(object subscriber, object delegateSource) { if (subscriber.Equals(delegateSource)) { // The target is the subscriber; we can use a weakreference DelegateWeakReference = new WeakReference(delegateSource); _isStrongReference = false; } else { DelegateStrongReference = delegateSource; _isStrongReference = true; } } public object Target => _isStrongReference ? DelegateStrongReference : DelegateWeakReference.Target; public bool IsAlive => _isStrongReference || DelegateWeakReference.IsAlive; } class Subscription : Tuple { public Subscription(object subscriber, object delegateSource, MethodInfo methodInfo, Filter filter) : base(new WeakReference(subscriber), new MaybeWeakReference(subscriber, delegateSource), methodInfo, filter) { } public WeakReference Subscriber => Item1; MaybeWeakReference DelegateSource => Item2; MethodInfo MethodInfo => Item3; Filter Filter => Item4; public void InvokeCallback(object sender, object args) { if (!Filter(sender)) { return; } if (MethodInfo.IsStatic) { MethodInfo.Invoke(null, MethodInfo.GetParameters().Length == 1 ? new[] { sender } : new[] { sender, args }); return; } var target = DelegateSource.Target; if (target == null) { return; // Collected } MethodInfo.Invoke(target, MethodInfo.GetParameters().Length == 1 ? new[] { sender } : new[] { sender, args }); } public bool CanBeRemoved() { return !Subscriber.IsAlive || !DelegateSource.IsAlive; } } readonly Dictionary> _subscriptions = new Dictionary>(); public static void Send(TSender sender, string message, TArgs args) where TSender : class { Instance.Send(sender, message, args); } void IMessagingCenter.Send(TSender sender, string message, TArgs args) { if (sender == null) throw new ArgumentNullException(nameof(sender)); InnerSend(message, typeof(TSender), typeof(TArgs), sender, args); } public static void Send(TSender sender, string message) where TSender : class { Instance.Send(sender, message); } void IMessagingCenter.Send(TSender sender, string message) { if (sender == null) throw new ArgumentNullException(nameof(sender)); InnerSend(message, typeof(TSender), null, sender, null); } public static void Subscribe(object subscriber, string message, Action callback, TSender source = null) where TSender : class { Instance.Subscribe(subscriber, message, callback, source); } void IMessagingCenter.Subscribe(object subscriber, string message, Action callback, TSender source) { if (subscriber == null) throw new ArgumentNullException(nameof(subscriber)); if (callback == null) throw new ArgumentNullException(nameof(callback)); var target = callback.Target; Filter filter = sender => { var send = (TSender)sender; return (source == null || send == source); }; InnerSubscribe(subscriber, message, typeof(TSender), typeof(TArgs), target, callback.GetMethodInfo(), filter); } public static void Subscribe(object subscriber, string message, Action callback, TSender source = null) where TSender : class { Instance.Subscribe(subscriber, message, callback, source); } void IMessagingCenter.Subscribe(object subscriber, string message, Action callback, TSender source) { if (subscriber == null) throw new ArgumentNullException(nameof(subscriber)); if (callback == null) throw new ArgumentNullException(nameof(callback)); var target = callback.Target; Filter filter = sender => { var send = (TSender)sender; return (source == null || send == source); }; InnerSubscribe(subscriber, message, typeof(TSender), null, target, callback.GetMethodInfo(), filter); } public static void Unsubscribe(object subscriber, string message) where TSender : class { Instance.Unsubscribe(subscriber, message); } void IMessagingCenter.Unsubscribe(object subscriber, string message) { InnerUnsubscribe(message, typeof(TSender), typeof(TArgs), subscriber); } public static void Unsubscribe(object subscriber, string message) where TSender : class { Instance.Unsubscribe(subscriber, message); } void IMessagingCenter.Unsubscribe(object subscriber, string message) { InnerUnsubscribe(message, typeof(TSender), null, subscriber); } void InnerSend(string message, Type senderType, Type argType, object sender, object args) { if (message == null) throw new ArgumentNullException(nameof(message)); var key = new Sender(message, senderType, argType); if (!_subscriptions.ContainsKey(key)) return; List subcriptions = _subscriptions[key]; if (subcriptions == null || !subcriptions.Any()) return; // should not be reachable // ok so this code looks a bit funky but here is the gist of the problem. It is possible that in the course // of executing the callbacks for this message someone will subscribe/unsubscribe from the same message in // the callback. This would invalidate the enumerator. To work around this we make a copy. However if you unsubscribe // from a message you can fairly reasonably expect that you will therefor not receive a call. To fix this we then // check that the item we are about to send the message to actually exists in the live list. List subscriptionsCopy = subcriptions.ToList(); foreach (Subscription subscription in subscriptionsCopy) { if (subscription.Subscriber.Target != null && subcriptions.Contains(subscription)) { subscription.InvokeCallback(sender, args); } } } void InnerSubscribe(object subscriber, string message, Type senderType, Type argType, object target, MethodInfo methodInfo, Filter filter) { if (message == null) throw new ArgumentNullException(nameof(message)); var key = new Sender(message, senderType, argType); var value = new Subscription(subscriber, target, methodInfo, filter); if (_subscriptions.ContainsKey(key)) { _subscriptions[key].Add(value); } else { var list = new List { value }; _subscriptions[key] = list; } } void InnerUnsubscribe(string message, Type senderType, Type argType, object subscriber) { if (subscriber == null) throw new ArgumentNullException(nameof(subscriber)); if (message == null) throw new ArgumentNullException(nameof(message)); var key = new Sender(message, senderType, argType); if (!_subscriptions.ContainsKey(key)) return; _subscriptions[key].RemoveAll(sub => sub.CanBeRemoved() || sub.Subscriber.Target == subscriber); if (!_subscriptions[key].Any()) _subscriptions.Remove(key); } // This is a bit gross; it only exists to support the unit tests in PageTests // because the implementations of ActionSheet, Alert, and IsBusy are all very // tightly coupled to the MessagingCenter singleton internal static void ClearSubscribers() { (Instance as MessagingCenter)?._subscriptions.Clear(); } } }