Why This Pattern?
Publish–Subscribe (Pub/Sub) is like the PA system for your app: one bit of code (the publisher) shouts, “Hey, something just happened!” and anyone who cares (subscribers) quietly tunes in. Nobody swaps phone numbers, so publishers stay blissfully ignorant of who’s listening, and subscribers don’t sweat who triggered the event. That double-blind decoupling is catnip for event-driven or distributed setups.
In a statically-typed world like C#, Pub/Sub is perfect when you need to fire off work without tripping over other patterns—or the UI thread. Strong typing keeps your contracts clear, while async subscribers do their thing off to the side so the main flow stays snappy.
Think real-world stuff: an IoT sensor pings your app, a background job waits for a status flip before nudging the UI, or your backend shoots a push notification. For all those “react-as-soon-as-this-happens” moments, Pub/Sub is the buddy you’ll call first.
Recommended Libraries
Pub/Sub choices in .NET MAUI aren’t exactly scarce, but two names pop up in just about every repo and blog post you’ll stumble across.
CommunityToolkit.Mvvm WeakReferenceMessenger – bundled with the official MAUI Community Toolkit, it keeps event contracts strongly-typed while holding references loosely so your pages don’t stick around in memory after you navigate away. Fire-and-forget, minimal ceremony, shipped by the same folks who maintain the toolkit you’re already using for MVVM goodness. Ref: WeakMessaging
Plugin.Maui.MessagingCenter – a lightweight, MAUI-friendly wrapper around the classic Xamarin .Forms MessagingCenter. Same dead-simple .Send() / .Subscribe() API you might already know, refreshed for the multi-target MAUI world with no extra dependencies. Ref: MessagingCenter By Gerald
Both libraries let your pages, view-models, and services gossip without swapping phone numbers: publishers broadcast, subscribers listen, and nobody keeps a hard reference to anyone else. They’re thread-safe, play nicely with async/await, and live entirely in-process—no broker, queues, or JSON schemas required. Pick the Toolkit if you want strong typing and built-in leak protection, or stick with MessagingCenter if you favor the ultra-simple “string topic + payload” vibe. Either way, you get that double-blind decoupling MAUI apps crave.
Good Practices
There are 3 things that you can do to improve your Pub/Sub pattern. Again this practices are based on my dev experience, they can be better or other ref options, my intention in share my knowledge instead override others. Keep that in mind.
- Usage on Handlers or Platform Specific
- Usage for Core Layer or ViewModels
- Example of messaging payload
Now let's code a little bit...
Usage on Handlers or Platform Specific
What you can do
When I’m wiring up custom handlers, Pub / Sub is my grab-and-go trick. Think of an Android barcode-scanner handler shouting “QR decoded!” while the shared view happily renders the result—no ugly #if ANDROID needed. Over on iOS, a Core Bluetooth handler yells “BLE connected,” Windows fires “pen pressure,” and the same shared code just listens. I can bolt on native NFC taps or game-controller events without splashing platform glue across my neutral layer.
That recipe’s worked for me—your mileage may vary. I’m not laying down gospel here, just sharing what’s saved me headaches. With Pub/Sub all I track is a topic name. Swap out an Android Binding Library tweak for an iOS Binding Library shim, add a fresh Windows/MacOS target, and the rest of the app doesn’t flinch. Tests spoof messages, new platforms add their own handler publishers, and my shared codebase keeps its zen-garden cleanliness.
What you should avoid
Stuff I dodge when wiring handlers to the bus: never blast messages every frame from a gyro stream—debounce or you’ll tank battery and FPS. Unhook in DisconnectHandler/Dispose/OnNavigatedFrom; leaked subscriptions survive orientation changes and eat RAM. Keep payloads skinny—pass a file path, not the 4 MB photo bytes. Don’t bury runtime-permission prompts inside callbacks; raise an event and let a page decide or task completions will vanish on suspend/resume. And always program for the void—if a device lacks haptics, your “vibrate” message may meet exactly zero listeners.
Usage for Core Layer or ViewModels
What you can do
In my .NET MAUI apps, wiring Pub/Sub between services and ViewModels (when necessary) keeps things nice and loose: the page fires an event, any background service handles it on its own thread, and the UI never hangs. That decoupling lets me push silent refreshes, live notifications, or analytics pings without sprinkling callbacks all over the codebase—smooth user flow, fewer spaghetti dependencies.
These thoughts come straight from projects I’ve wrestled with; you might find slicker tricks or stronger refs elsewhere, and that’s cool. What Pub/Sub still buys me is painless testing (just inject a mock bus), cheap feature adds (new view just subscribes), and safer refactors because publishers never care who’s on the other end.
What you should avoid
Things I try hard avoid: don’t blast huge payloads through the bus—pass IDs and pull details locally or you’ll choke memory. Resist treating Pub/Sub as a magic DI container: if a consumer is required, wire it directly so failures surface fast. Keep the messenger wrapped in a thin service so tests stay clean, and always unsubscribe; orphan handlers pin pages in memory and wreck perf on Android back-stacks. Skip heavy business logic in the callback—hand it off to a service or background task—and never assume UI-thread context; hop back to MainThread or enjoy random crashes.
Example of messaging payload
If you are looking some Pub/Sub implementation code in .NET MAUI, this is your section. Now there is two libraries mention in this post, so I will created code for both of them. Are you ready?
Plugin.Maui.MessagingCenter
Let's create the Subscribe and Unsubscribe functions in the class that you will Subscribe/Register the payload.
/// <summary>
/// Subscribes to MessagingCenter messages carrying a MessagingResponse payload.
/// </summary>
public void Subscribe()
{
MessagingCenter.Subscribe<object, MessagingResponse>(
this,
nameof(MessagingResponse),
DoWhenMessageReceived);
}
/// <summary>
/// Unsubscribes from MessagingCenter messages.
/// </summary>
public void Unsubscribe()
{
MessagingCenter.Unsubscribe<object, MessagingResponse>(
this,
nameof(MessagingResponse));
}
Now, add the next model to your project or Entity layer. This will gives to every Pub/Sub hop a single, consistent envelope: Result carries data, MessageType hints at shape, and built-in error/exception flags travel with it, so subscribers can act or bail fast. One strongly-typed wrapper means less casting, safer logging, and cleaner testing across MAUI targets.
If you have a global constant class, add the enum there.
public class MessagingResponse
{
public enum MessageType
{
SingleResponse,
ComplexResponse
}
public object? Result { get; set; }
public bool HasError { get; set; }
public bool HasException { get; set; }
public string? ErrorMessage { get; set; }
public Exception? Exception { get; set; }
public MessageType MessageType { get; set; }
}
The next thing to do is define what you need when the Subscription get hits. I did a sample function for you:
/// <summary>
/// Handles incoming MessagingResponse messages.
/// </summary>
/// <param name="sender">The publisher sending the message.</param>
/// <param name="message">The message payload.</param>
private async void DoWhenMessageReceived(object sender, MessagingResponse message)
{
if (message.HasException || message.HasError)
{
Console.WriteLine($"Do Something when error or exception: {message.Exception?.Message} {message.ErrorMessage}");
return;
}
switch (message.MessageType)
{
case MessageType.SingleResponse:
// Example: message.Result = https://www.google.com to Ping Google in the background
try
{
if(message.Result == null)
return;
using var client = new HttpClient();
var response = await client.GetAsync((string)message.Result));
Console.WriteLine($"Website ping status: {response.StatusCode}");
}
catch (Exception ex)
{
Console.WriteLine($"Ping failed: {ex.Message}");
}
break;
case MessageType.ComplexResponse:
// Handle complex objects here. Ex: Iterate a list, call a services, or another kind of sync/async activity.
break;
}
}
Finally, do not forget to call using Plugin.Maui.MessagingCenter;
in your class or implement your publishHelper or service to the send the messages from your most convenient case:
public void Subscribe() =>
MessagingCenter.Subscribe<object, MessagingResponse>(
this,
nameof(MessagingResponse),
DoWhenMessageReceived);
public void Unsubscribe() =>
MessagingCenter.Unsubscribe<object, MessagingResponse>(
this,
nameof(MessagingResponse));
CommunityToolkit.Mvvm WeakReferenceMessenger
Let's reuse the previous example, first add the library to and nuget package for using CommunityToolkit.Mvvm.Messaging;
to Send a message:
/// <summary>
/// Publishes this <see cref="MessagingResponse"/> instance through the toolkit messenger.
/// </summary>
public void Send() => WeakReferenceMessenger.Default.Send(this);
/// <summary>
/// Static helper to publish any <see cref="MessagingResponse"/> payload.
/// </summary>
public static void Send(MessagingResponse payload) => WeakReferenceMessenger.Default.Send(payload);
It's more or less the same than previous to Subscribed and unsubscribe:
public void Subscribe() =>
WeakReferenceMessenger.Default.Register<MessagingResponse>(
this,
(recipient, message) => _ = HandleMessageAsync(message));
public void Unsubscribe() =>
WeakReferenceMessenger.Default.Unregister<MessagingResponse>(this);
Finally, your to-do function:
private static async Task HandleMessageAsync(MessagingResponse message)
{
if (message.HasException || message.HasError)
{
Console.WriteLine($"Error or exception: {message.Exception?.Message ?? message.ErrorMessage}");
return;
}
switch (message.MessageType)
{
case MessageType.SingleResponse:
if (message.Result is string url)
{
try
{
using var client = new HttpClient();
var response = await client.GetAsync(url);
Console.WriteLine($"Website ping status: {response.StatusCode}");
}
catch (Exception ex)
{
Console.WriteLine($"Ping failed: {ex.Message}");
}
}
break;
case MessageType.ComplexResponse:
Console.WriteLine("Complex response processing...");
break;
}
}
I hope this could help you. Enjoy the coding!...
Credits To:
Many thanks to these cracks...
Ref: Gerald Versluis
Ref: CommunityToolkit.Mvvm Contributors
Top comments (0)