Implementing a simple messenger component for WPF, UWP and Xamarin

The Architecture of Messenger component

Message

public abstract class Message
{
public object Sender { get; private set; }
protected Message(object sender)
{
Sender = sender ?? throw new ArgumentNullException(nameof(sender));
}
}

Subscriptions

public abstract class BaseSubscription
{
public Guid Id { get; private set; }
public SubscriptionPriority Priority { get; private set; }
public string Tag { get; private set; }
public abstract Task<bool> Invoke(object message);
protected BaseSubscription(SubscriptionPriority priority, string tag)
{
Id = Guid.NewGuid();
Priority = priority;
Tag = tag;
}
}
public class StrongSubscription<TMessage> : BaseSubscription where TMessage : Message
{
private readonly Action<TMessage> _action;

public StrongSubscription(Action<TMessage> action,
SubscriptionPriority priority, string tag): base(priority, tag)
{
_action = action;
}
public override async Task<bool> Invoke(object message)
{
var typedMessage = message as TMessage;
if (typedMessage == null)
{
throw new Exception($"Unexpected message {message.ToString()}");
}
await Task.Run(() => _action?.Invoke(typedMessage));
return true;
}
}
public class WeakSubscription<TMessage> : BaseSubscription where TMessage : Message
{
private readonly WeakReference<Action<TMessage>> _weakReference;

public WeakSubscription(Action<TMessage> action,
SubscriptionPriority priority, string tag) : base(priority, tag)
{
_weakReference = new WeakReference<Action<TMessage>>(action);
}

public override async Task<bool> Invoke(object message)
{
var typedMessage = message as TMessage;
if (typedMessage == null)
{
throw new Exception($"Unexpected message {message.ToString()}");
}
Action<TMessage> action;
if (!_weakReference.TryGetTarget(out action))
{
return false;
}
await Task.Run(() => action?.Invoke(typedMessage));
return true;
}
}

MessengerHub

public class MessengerHub
{
private static readonly Lazy<MessengerHub> lazy = new Lazy<MessengerHub>(() => new MessengerHub());
private MessengerHub() { }
public static MessengerHub Instance
{
get
{
return lazy.Value;
}
}
}
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Guid, BaseSubscription>> _subscriptions =
new ConcurrentDictionary<Type, ConcurrentDictionary<Guid, BaseSubscription>>();

Subscribe

public SubscriptionToken Subscribe<TMessage>(Action<TMessage> action,
ReferenceType referenceType = ReferenceType.Weak,
SubscriptionPriority priority = SubscriptionPriority.Normal, string tag = null) where TMessage : Message
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
BaseSubscription subscription = BuildSubscription(action, referenceType, priority, tag);
return SubscribeInternal(action, subscription);
}

private SubscriptionToken SubscribeInternal<TMessage>(Action<TMessage> action, BaseSubscription subscription)
where TMessage : Message
{
if (!_subscriptions.TryGetValue(typeof(TMessage), out var messageSubscriptions))
{
messageSubscriptions = new ConcurrentDictionary<Guid, BaseSubscription>();
_subscriptions[typeof(TMessage)] = messageSubscriptions;
}
messageSubscriptions[subscription.Id] = subscription;
return new SubscriptionToken(subscription.Id, async () => await UnsubscribeInternal<TMessage>(subscription.Id), action);
}
public sealed class SubscriptionToken : IDisposable
{
public Guid Id { get; private set; }
private readonly Action _disposeMe;
private readonly object _dependentObject;

public SubscriptionToken(Guid id, Action disposeMe, object dependentObject)
{
Id = id;
_disposeMe = disposeMe;
_dependentObject = dependentObject;
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

private void Dispose(bool isDisposing)
{
if (isDisposing)
{
_disposeMe();
}
}
}

Unsubscribe

public async Task Unsubscribe<TMessage>(SubscriptionToken subscriptionToken) where TMessage : Message
{
await UnsubscribeInternal<TMessage>(subscriptionToken.Id);
}
private async Task UnsubscribeInternal<TMessage>(Guid subscriptionId) where TMessage : Message
{
if (_subscriptions.TryGetValue(typeof(TMessage), out var messageSubscriptions))
{
if (messageSubscriptions.ContainsKey(subscriptionId))
{
var result = messageSubscriptions.TryRemove(subscriptionId, out BaseSubscription value);
}
}
}

Publish

public async Task Publish<TMessage>(TMessage message) where TMessage : Message
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
List<BaseSubscription> toPublish = null;
Type messageType = message.GetType();

if (_subscriptions.TryGetValue(messageType, out var messageSubscriptions))
{
toPublish = messageSubscriptions.Values.OrderByDescending(x => x.Priority).ToList();
}

if (toPublish == null || toPublish.Count == 0)
{
return;
}

List<Guid> deadSubscriptionIds = new List<Guid>();
foreach (var subscription in toPublish)
{
// Execute the action for this message.
var result = await subscription.Invoke(message);
if (!result)
{
deadSubscriptionIds.Add(subscription.Id);
}
}

if (deadSubscriptionIds.Any())
{
await PurgeDeadSubscriptions(messageType, deadSubscriptionIds);
}
}

Usage

PM> Install-Package FunCoding.CoreMessenger
  • Publish:
public async Task Publish<TMessage>(TMessage message)
  • Subscribe:
public SubscriptionToken Subscribe<TMessage>(Action<TMessage> action, ReferenceType referenceType = ReferenceType.Weak, SubscriptionPriority priority = SubscriptionPriority.Normal, string tag = null)
  • Unsubscribe:
public async Task Unsubscribe<TMessage>(SubscriptionToken subscriptionToken)

Creating the Message class

public class TestMessage : Message
{
public string ExtraContent { get; private set; }
public TestMessage(object sender, string content) : base(sender)
{
ExtraContent = content;
}
}
var message = new TestMessage(this, "Test Content");

Subscription

public class HomeViewModel
{
private readonly SubscriptionToken _subscriptionTokenForTestMessage;
public HomeViewModel()
{
_subscriptionTokenForTestMessage =
MessengerHub.Instance.Subscribe<TestMessage>(OnTestMessageReceived,
ReferenceType.Weak, SubscriptionPriority.Normal);
}

private void OnTestMessageReceived(TestMessage message)
{
#if DEBUG
System.Diagnostics.Debug.WriteLine($"Received messages of type {message.GetType().ToString()}. Content: {message.Content}");
#endif
}
}

Publishing the Message

public async Task PublishMessage()
{
await MessengerHub.Instance.Publish(new TestMessage(this, $"Hello World!"));
}

Parameters

public SubscriptionToken Subscribe<TMessage>(Action<TMessage> action,
ReferenceType referenceType = ReferenceType.Weak,
SubscriptionPriority priority = SubscriptionPriority.Normal, string tag = null) where TMessage : Message
  • ReferenceType. The default value is ReferenceType.Weak so you do not need to worry about the memory leaking. Once the SubscriptionToken instance goes out of the scope, GC can collect it automatically(But not sure when). If you need to keep a strong reference, specify the parameter as ReferenceType.Strong so that GC cannot collect it.
  • SubscriptionPriority. The default value is SubscriptionPriority.Normal. Sometimes it is required to control the execution orders of the subscriptions for one Message. In this case, specify different priorities for the subscriptions to control the execution orders. Notice that this parameter is not for different Messages.
  • Tag. It is optional to inspect the current status for subscriptions.

Unsubscribe

  • Use Unsubscribe method, as shown below:
await MessengerHub.Instance.Unsubscribe<TestMessage>(_subscriptionTokenForTestMessage);
  • Use Dispose method of the SubscriptionToken:
_subscriptionTokenForTestMessage.Dispose();
public void MayNotEverReceiveAMessage()
{
var token = MessengerHub.Instance.Subscribe<TestMessage>((message) => {
// Do something here
});
// token goes out of scope now
// - so will be garbage collected *at some point*
// - so the action may never get called
}

Differences with MvvmCross.Messenger

--

--

--

Microsoft MVP / .NET, Azure Developer / Learner

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

The Best Way To Learn Any Technology

Open-source style community engagement for the Data Commons Pilot Phase Consortium

Alteryx *.yxi offline installer — Bypass the network block for PyPI tool

Working with Kotlin Enums

A ‘what you need’ guide to understand anything — Coding

Fix Chia Blockchain installation

Connect to your Telegram Lamden account

5 CI/CD Best Practices

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Xiaodi Yan

Xiaodi Yan

Microsoft MVP / .NET, Azure Developer / Learner

More from Medium

C# Interface and why Interface

Relationships in EF Core:

Pangram in C#