The Event Aggregator Pattern in .NET MAUI: Building Loosely Coupled Communication
The Event Aggregator Pattern in .NET MAUI
Building Loosely Coupled Communication
As your .NET MAUI app grows, direct references between ViewModels start becoming architectural debt.
Tightly coupled communication is one of the most common scaling problems in client applications. ViewModels referencing each other directly, services triggering UI logic, and navigation layers leaking state across the app. The Event Aggregator pattern solves this by introducing a centralized messaging hub that enables components to communicate without knowing about each other. Letβs break it down.
π§ The Problem: Tight Coupling
Consider this scenario:
// β Direct coupling
public class OrdersViewModel
{
private readonly DashboardViewModel _dashboard;
public OrdersViewModel(DashboardViewModel dashboard)
{
_dashboard = dashboard;
}
public void CompleteOrder()
{
_dashboard.RefreshStats();
}
}
Problems:
- Hard dependency
- Breaks separation of concerns
- Difficult to test
- Hidden ripple effects
Now imagine 15 ViewModels interacting like this.
π― The Goal
We want: β Decoupled communication
β No direct references
β Testable architecture
β Scalable message distribution
β Clear separation of concerns
π§© What Is the Event Aggregator Pattern?
An Event Aggregator is a mediator that:
- Allows publishers to broadcast events
- Allows subscribers to listen for events
- Removes direct dependencies between components
Think of it as:
A strongly-typed in-memory message bus.
π Step 1 β Define the Event
Events are simple message objects.
public class OrderCompletedEvent
{
public int OrderId { get; }
public OrderCompletedEvent(int orderId)
{
OrderId = orderId;
}
}
No logic. Just data.
π Step 2 β Define the Event Aggregator Interface
public interface IEventAggregator
{
void Publish<TEvent>(TEvent @event);
void Subscribe<TEvent>(Action<TEvent> handler);
void Unsubscribe<TEvent>(Action<TEvent> handler);
}
π Step 3 β Implement the Aggregator
Minimal implementation:
public class EventAggregator : IEventAggregator
{
private readonly Dictionary<Type, List<Delegate>> _subscriptions = new();
public void Publish<TEvent>(TEvent @event)
{
if (_subscriptions.TryGetValue(typeof(TEvent), out var handlers))
{
foreach (var handler in handlers.Cast<Action<TEvent>>())
handler(@event);
}
}
public void Subscribe<TEvent>(Action<TEvent> handler)
{
if (!_subscriptions.ContainsKey(typeof(TEvent)))
_subscriptions[typeof(TEvent)] = new List<Delegate>();
_subscriptions[typeof(TEvent)].Add(handler);
}
public void Unsubscribe<TEvent>(Action<TEvent> handler)
{
if (_subscriptions.TryGetValue(typeof(TEvent), out var handlers))
handlers.Remove(handler);
}
}
Register it in DI:
builder.Services.AddSingleton<IEventAggregator, EventAggregator>();
π Step 4 β Publish an Event
public class OrdersViewModel
{
private readonly IEventAggregator _events;
public OrdersViewModel(IEventAggregator events)
{
_events = events;
}
public void CompleteOrder(int orderId)
{
_events.Publish(new OrderCompletedEvent(orderId));
}
}
π Step 5 β Subscribe to the Event
public class DashboardViewModel
{
private readonly IEventAggregator _events;
public DashboardViewModel(IEventAggregator events)
{
_events = events;
_events.Subscribe<OrderCompletedEvent>(OnOrderCompleted);
}
private void OnOrderCompleted(OrderCompletedEvent evt)
{
RefreshStats();
}
}
No reference between ViewModels. Clean.
π§ Why This Matters in MAUI
In MAUI apps:
- Pages come and go
- ViewModels are short-lived
- Navigation stacks change
- Background services may trigger UI updates
Event Aggregator: β Prevents navigation-based coupling
β Avoids service β UI references
β Simplifies cross-module communication
β Improves testability
β οΈ Important Considerations
1οΈβ£ Memory Leaks
If you donβt unsubscribe:
_events.Unsubscribe
You risk retaining ViewModels in memory. Better approach:
- Use weak references
- Or integrate IDisposable pattern
2οΈβ£ Thread Safety
If publishing from background threads:
- Ensure handlers marshal back to MainThread
Example:
MainThread.BeginInvokeOnMainThread(() =>
{
RefreshStats();
});
3οΈβ£ Avoid Event Chaos
Event Aggregator is powerful. But: β Donβt turn everything into events
β Donβt use it for direct command flows
β Donβt hide core business logic inside handlers Use it for: β Cross-cutting notifications
β State changes
β Domain events
β Decoupled reactions
π Event Aggregator vs MessagingCenter
Older Xamarin.Forms apps used:
MessagingCenter.Send(...)
In MAUI:
- MessagingCenter is deprecated.
- Event Aggregator is strongly typed.
- Cleaner and testable.
- Better suited for DI.
π§ͺ Testing Becomes Simple
You can mock:
var mockEvents = new Mock<IEventAggregator>();
And verify:
mockEvents.Verify(x => x.Publish(It.IsAny<OrderCompletedEvent>()));
Clean unit tests.
π Final Thoughts
The Event Aggregator pattern:
- Reduces architectural friction
- Encourages modular design
- Enables scalable ViewModel communication
- Improves long-term maintainability
In small apps, you may not need it. In growing MAUI applications? It becomes architectural glue. And when used correctly: It disappears β which is exactly what good architecture should do.
