Table of Contents
C# View Model Messenger Service
When building a WPF application using the Inversion of Control (IOC) Principle with dependency injection we often need to inject view models into view models so we can share the data, events or property’s. This tightly couples the view models together which violates the IOC principle. To get around this we want to use the Messenger Service pattern to communicate between the view models and share data.
Messenger Service
Why Use a Custom Messaging System?
Off-the-shelf messaging solutions may not always align perfectly with the requirements of your web application. By developing a custom messaging system, you gain full control over how messages are sent, received, and processed. This enables you to tailor the messaging system to the specific needs of your application, resulting in better performance and scalability.
Key Components of the Messaging System
- Messenger Class: The heart of the messaging system, the Messenger class provides methods for sending and subscribing to messages of various types.
- Subscription Record: A simple record type that encapsulates a subscriber object and the action to be performed when a message is received.
Functionality Overview
- Sending Messages: The Send method allows messages of any type to be sent to subscribers. It updates the current state of the message and notifies all subscribers associated with the message type.
- Subscribing to Messages: The Subscribe method enables objects to subscribe to messages of a specific type. Subscribers provide an action that is executed when a message of the subscribed type is received.
- Unsubscribing from Messages: Subscribers can unsubscribe from receiving messages of a particular type using the Unsubscribe method.
Integration with WPF Applications
- Real-time Updates: Integrate the messaging system to provide real-time updates to users, such as live chat features, notifications, or dynamic content updates.
- Event Triggering: Use the messaging system to trigger events based on user actions or system events, enhancing the interactivity of your web application.
- Customized Interactions: Tailor the messaging system to meet the specific requirements of your web application, ensuring seamless integration and optimal performance.
Messenger Service Code
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Collections.ObjectModel; public interface IMessenger { void Send<TMessage>(TMessage message); void Subscribe<TMessage>(object subscriber, Action<object> action); void UnSubscribe<TMessage>(object subscriber); } /// <summary> /// Defines a simple messaging system for sending and receiving messages of various types. /// </summary> public class Messenger : IMessenger { // Dictionary to hold subscriptions for different message types private ConcurrentDictionary<Type, SynchronizedCollection<Subscription>> _subscriptions = new ConcurrentDictionary<Type, SynchronizedCollection<Subscription>>(); // Dictionary to hold the current state of messages private ConcurrentDictionary<Type, object> _currentState = new ConcurrentDictionary<Type, object>(); /// <summary> /// Initializes a new instance of the Messenger class. /// </summary> public Messenger() { } /// <summary> /// Sends a message of type TMessage to all subscribers. /// </summary> /// <typeparam name="TMessage">The type of the message being sent.</typeparam> /// <param name="message">The message to send.</param> public void Send<TMessage>(TMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); // Add or update the current state for the message type _currentState.AddOrUpdate(typeof(TMessage), (o) => message, (o, old) => message); // Invoke each subscriber's action for the given message type if (_subscriptions.TryGetValue(typeof(TMessage), out var subscriptions)) { foreach (var subscription in subscriptions) { subscription.action(message); } } } /// <summary> /// Subscribes an object to receive messages of type TMessage. /// </summary> /// <typeparam name="TMessage">The type of the message to subscribe to.</typeparam> /// <param name="subscriber">The object subscribing to the message.</param> /// <param name="action">The action to perform when a message is received.</param> public void Subscribe<TMessage>(object subscriber, Action<object> action) { // Create a new subscription Subscription newSubscriber = new Subscription(subscriber, action); // Add the subscription to the list of subscriptions for the message type var subscriptions = _subscriptions.GetOrAdd(typeof(TMessage), new SynchronizedCollection<Subscription>()); subscriptions.Add(newSubscriber); // If there's a current state for the message type, trigger the action immediately if (_currentState.TryGetValue(typeof(TMessage), out var currentState)) { action(currentState); } } /// <summary> /// Unsubscribes an object from receiving messages of type TMessage. /// </summary> /// <typeparam name="TMessage">The type of the message to unsubscribe from.</typeparam> /// <param name="subscriber">The object to unsubscribe.</param> public void UnSubscribe<TMessage>(object subscriber) { // Check if there are subscriptions for the message type if (_subscriptions.TryGetValue(typeof(TMessage), out var subscriptions)) { // Find and remove the subscription associated with the subscriber var subscriptionToRemove = subscriptions.FirstOrDefault(s => s.subscriber == subscriber); if (subscriptionToRemove != null) { subscriptions.Remove(subscriptionToRemove); } } } /// <summary> /// Represents a subscription to a message type. /// </summary> public record Subscription(object subscriber, Action<object> action); }
Integrating into a WPF Application
See my post on dependency injection if you need some help.
App.Xaml.cs
In the services section of the App.XAML.cs file we need to AddSingleton for the Messenger service as shown below.
_host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { services.AddSingleton(m => new MainWindow()); services.AddSingleton(_logger); services.AddSingleton<IMainViewModel, MainViewModel>(); services.AddSingleton<IMessenger, Messenger>(); }) .UseSerilog() .Build();
Message Records
I added a folder for messages and created a new class called MessageRecords. The class it’s self gets deleted so we can create this standalone record object. The OnlineStatusChangedMessage record represents a message used within a messaging system to convey changes in online status. Specifically, it encapsulates a boolean value indicating whether a user has transitioned to an online state or not.
Subscribing View Model
Within a View Model like the Main View Model we add a reference in the constructor to the Messenger interface.
public class MainViewModel:ViewModelBase,IMainViewModel { private readonly ILogger _logger; private readonly IMessenger _messenger; public MainWindow? MainWindow { get; } = null; public MainViewModel( ILogger logger, IMessenger messenger, MainWindow mainWindow ) { logger.Information("Application Started."); _logger = logger; _messenger = messenger; MainWindow = mainWindow; } }
namespace User_Controls.Messages { public record OnlineStatusChangedMessage(bool isOnLine); }
Updating the Property
We can then add a subscription and a method to handle the message.
public class MainViewModel:ViewModelBase,IMainViewModel { private readonly ILogger _logger; private readonly IMessenger _messenger; public MainWindow? MainWindow { get; } = null; public MainViewModel( ILogger logger, IMessenger messenger, MainWindow mainWindow ) { logger.Information("Application Started."); _logger = logger; _messenger = messenger; _messenger.Subscribe<OnlineStatusChangedMessage>(this, OnlineStatusChanged); MainWindow = mainWindow; } private void OnlineStatusChanged(object obj) { var message = (OnlineStatusChangedMessage)obj; SetOnlineStatus(message.isOnLine); } private bool _setOnlineStatus =false; public bool SetOnlineStatus(bool value) { _setOnlineStatus = value; return _setOnlineStatus; } }
Sending the Message
From another view model we can now use the Message Record to send a message to the Main View Model that the Online Status has changed.
_messenger.Send(new OnlineStatusChangedMessage(true));
Conclusion
Implementing a custom messaging system in C# empowers developers to create highly interactive and responsive WPF applications. By leveraging the flexibility and control offered by a custom solution, developers can design messaging systems that seamlessly integrate with their applications, delivering enhanced user experiences and improved functionality.
With the insights provided in this article, you’re equipped to explore the possibilities of integrating a custom messaging system into your WPF applications, unlocking new avenues for real-time communication and interaction.
Example
App.xaml
<Application x:Class="TestApp.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:TestApp" Startup="OnStartup"> <Application.Resources> </Application.Resources> </Application>
App.xaml.cs
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using System.IO; using System.Text; using System.Windows; using TestApp.Services; using TestApp.Services.ViewModels; using TestApp.Stores; using TestApp.ViewModels; namespace TestApp { public partial class App : Application { private MainWindow? _mainwindow; private ILogger? _logger; private IHost? _host; private void OnStartup(object sender, StartupEventArgs e) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); IConfigurationBuilder builder = new ConfigurationBuilder(); BuildConfig(builder); _logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Build()) .Enrich.FromLogContext() .CreateLogger(); _logger.Information("App - OnStartup - Application Starting"); _logger.Information("App - OnStartup - Adding Dependancies"); _host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { services.AddSingleton(_logger); services.AddSingleton(m => new MainWindow()); services.AddSingleton(n => new NavigationStore()); services.AddSingleton<IMainViewModel, MainViewModel>(); services.AddSingleton<IMessengerService, MessengerService>(); services.AddSingleton<ILeftViewModel, LeftViewModel>(); services.AddSingleton<IRightViewModel, RightViewModel>(); }) .UseSerilog() .Build(); _logger.Information("App - OnStartup - Creating the Main UI."); _mainwindow = _host.Services.GetRequiredService<MainWindow>(); _logger.Information("App - OnStartup - Setting the UI DataContext."); _mainwindow.DataContext = ActivatorUtilities.CreateInstance<MainViewModel>(_host.Services); _logger.Information("App - OnStartup - Showing UI."); _mainwindow.Show(); } private static void BuildConfig(IConfigurationBuilder builder) { builder.SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true) .AddEnvironmentVariables(); } } }
Commands Base
using System.Windows.Input; namespace TestApp.Commands.Base { public class CommandsBase : ICommand { private readonly Action<object?> _executeAction; private readonly Predicate<object?>? _canExecuteAction; public CommandsBase(Action<object?> executeAction) { _executeAction = executeAction; _canExecuteAction = null; } public CommandsBase(Action<object?> executeAction, Predicate<object?> canExecute) { _executeAction = executeAction; _canExecuteAction = canExecute; } public event EventHandler? CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public bool CanExecute(object? parameter) { return _canExecuteAction == null ? true : _canExecuteAction(parameter); } public void Execute(object? parameter) { if (_executeAction != null) { _executeAction(parameter); } } } }
View Model Base
using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows; namespace TestApp.ViewModels.Base { public interface IViewModelBase { Window? parentWindow { get; set; } IViewModelBase? ParentViewModel { get; set; } event PropertyChangedEventHandler? PropertyChanged; } public abstract class ViewModelBase : INotifyPropertyChanged, IViewModelBase { public Window? parentWindow { get; set; } = null; public IViewModelBase? ParentViewModel { get; set; } = null; public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName]string propertyName="") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public ViewModelBase() { } public ViewModelBase(IViewModelBase viewModelBase) { ParentViewModel = viewModelBase; } } }
Message Records
using TestApp.ViewModels.Base; namespace User_Controls.Messages { public record LeftTextSentMessage(string text); public record RightTextSentMessage(string text); public record ActivateLeftView(ViewModelBase viewModel); public record ActivateRightView(ViewModelBase viewModel); }
Navigation Store
using TestApp.ViewModels.Base; namespace TestApp.Stores { public interface INavigationStore { ViewModelBase? CurrentViewModel { get; set; } event Action? CurrentViewModelChanged; } public class NavigationStore : INavigationStore { public ViewModelBase? _CurrentViewModel; public ViewModelBase? CurrentViewModel { get => _CurrentViewModel; set { _CurrentViewModel = value; OnCurrentViewModelChanged(); } } public event Action? CurrentViewModelChanged; private void OnCurrentViewModelChanged() { CurrentViewModelChanged?.Invoke(); } } }
Left View Model
using Serilog; using System.Collections.ObjectModel; using System.Windows.Input; using TestApp.Commands.Base; using TestApp.Services; using TestApp.ViewModels.Base; using User_Controls.Messages; namespace TestApp.ViewModels { public interface ILeftViewModel { ObservableCollection<string> TextList { get; } ICommand? Submit { get; } ICommand? Switch { get; } string UserText { get; set; } } public class LeftViewModel:ViewModelBase,ILeftViewModel { private readonly ILogger _logger; private readonly IMessengerService _messengerService; public LeftViewModel( ILogger logger, IMessengerService messengerService ) { logger.Information("Application Started."); _logger = logger; _messengerService = messengerService; _messengerService.Subscribe<LeftTextSentMessage>(this, LeftTextSentMessage); } private void LeftTextSentMessage(object obj) { var message = (LeftTextSentMessage)obj; TextList.Add(message.text); } private ObservableCollection<string> _textList = new ObservableCollection<string>(); public ObservableCollection<string> TextList { get => _textList; set { _textList = value; OnPropertyChanged(); } } private string _userText = string.Empty; public string UserText { get => _userText; set { _userText = value; OnPropertyChanged(); } } public ICommand? Submit => new CommandsBase(SubmitCommandExecute); private void SubmitCommandExecute(object? obj) { _messengerService.Send(new RightTextSentMessage(UserText)); UserText = string.Empty; } public ICommand? Switch => new CommandsBase(SwitchCommandExecute); private void SwitchCommandExecute(object? obj) { _messengerService.Send(new ActivateLeftView(this)); } } }
Right View Model
using Serilog; using System.Collections.ObjectModel; using System.Windows.Input; using TestApp.Commands.Base; using TestApp.Services; using TestApp.ViewModels.Base; using User_Controls.Messages; namespace TestApp.ViewModels { public interface IRightViewModel { ObservableCollection<string> TextList { get; } ICommand? Submit { get; } ICommand? Switch { get; } string UserText { get; set; } } public class RightViewModel:ViewModelBase,IRightViewModel { private readonly ILogger _logger; private readonly IMessengerService _messengerService; public RightViewModel( ILogger logger, IMessengerService messengerService ) { logger.Information("Application Started."); _logger = logger; _messengerService = messengerService; _messengerService.Subscribe<RightTextSentMessage>(this, RightTextSentMessage); } private void RightTextSentMessage(object obj) { var message = (RightTextSentMessage)obj; TextList.Add(message.text); } private ObservableCollection<string> _textList = new ObservableCollection<string>(); public ObservableCollection<string> TextList { get => _textList; set { _textList = value; OnPropertyChanged(); } } private string _userText = string.Empty; public string UserText { get => _userText; set { _userText = value; OnPropertyChanged(); } } public ICommand? Submit => new CommandsBase(SubmitCommandExecute); private void SubmitCommandExecute(object? obj) { _messengerService.Send(new LeftTextSentMessage(UserText)); UserText = string.Empty; } public ICommand? Switch => new CommandsBase(SwitchCommandExecute); private void SwitchCommandExecute(object? obj) { _messengerService.Send(new ActivateRightView(this)); } } }
Main View Model
using Serilog; using TestApp.Stores; using TestApp.ViewModels; using TestApp.ViewModels.Base; using User_Controls.Messages; namespace TestApp.Services.ViewModels { public interface IMainViewModel { IViewModelBase? CurrentViewModel { get; } MainWindow? MainWindow { get; } INavigationStore NavigationStore { get; } ILeftViewModel LeftViewModel { get; } IRightViewModel RightViewModel { get; } } public class MainViewModel : ViewModelBase, IMainViewModel { private readonly ILogger _logger; private readonly IMessengerService _messengerService; public MainWindow? MainWindow { get; } = null; public INavigationStore NavigationStore { get; } public IViewModelBase? CurrentViewModel => NavigationStore.CurrentViewModel; public ILeftViewModel LeftViewModel { get; } public IRightViewModel RightViewModel { get; } public MainViewModel( ILogger logger, IMessengerService messengerService, ILeftViewModel leftViewModel, IRightViewModel rightViewModel, MainWindow mainWindow, NavigationStore navigationStore ) { logger.Information("Application Started."); _logger = logger; _messengerService = messengerService; _messengerService.Subscribe<ActivateLeftView>(this, ChangeActiveView); _messengerService.Subscribe<ActivateRightView>(this, ChangeActiveView); LeftViewModel = leftViewModel; RightViewModel = rightViewModel; MainWindow = mainWindow; NavigationStore = navigationStore; NavigationStore.CurrentViewModel = (ViewModelBase)LeftViewModel; NavigationStore.CurrentViewModelChanged += OnCurrentViewModelChanged; } private void ChangeActiveView(object obj) { switch (obj) { case ActivateLeftView _: NavigationStore.CurrentViewModel = (ViewModelBase)RightViewModel; break; case ActivateRightView _: NavigationStore.CurrentViewModel = (ViewModelBase)LeftViewModel; break; } } private void OnCurrentViewModelChanged() { OnPropertyChanged(nameof(CurrentViewModel)); } } }
Left View
<UserControl x:Class="TestApp.Views.LeftView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:TestApp.Views" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0" Orientation="Vertical"> <Label Content="Left View" Foreground="White" FontSize="35"/> <Label Content="Enter text" Foreground="White" Margin="10 10 0 0"/> <TextBox x:Name="txtBox" HorizontalAlignment="Left" VerticalAlignment="Top" Height="23" Width="120" Margin="10,10,0,0" TextWrapping="Wrap" Text="{Binding UserText}"/> <Button x:Name="btn" Content="Click me" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Height="23" Margin="10,20,0,0" Command="{Binding Submit}"/> <Button x:Name="switch" Content="Change View" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Height="23" Margin="10,20,0,0" Command="{Binding Switch}"/> </StackPanel> <ListView Grid.Column="1" x:Name="listView" HorizontalAlignment="Left" Height="400" Margin="10,10,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding TextList}"> <ListView.View> <GridView> <GridViewColumn Header="Text" DisplayMemberBinding="{Binding}" /> </GridView> </ListView.View> </ListView> </Grid> </UserControl>
Right View
<UserControl x:Class="TestApp.Views.RightView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0" Orientation="Vertical"> <Label Content="Right View" Foreground="White" FontSize="35"/> <Label Content="Enter text" Foreground="White" Margin="10 10 0 0"/> <TextBox x:Name="txtBox" HorizontalAlignment="Left" VerticalAlignment="Top" Height="23" Width="120" Margin="10,10,0,0" TextWrapping="Wrap" Text="{Binding UserText}"/> <Button x:Name="btn" Content="Click me" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Height="23" Margin="10,20,0,0" Command="{Binding Submit}"/> <Button x:Name="switch" Content="Change View" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Height="23" Margin="10,20,0,0" Command="{Binding Switch}"/> </StackPanel> <ListView Grid.Column="1" x:Name="listView" HorizontalAlignment="Left" Height="400" Margin="10,10,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding TextList}"> <ListView.View> <GridView> <GridViewColumn Header="Text" DisplayMemberBinding="{Binding}" /> </GridView> </ListView.View> </ListView> </Grid> </UserControl>
appsettings.json (Content, Copy if Newer)
{ "Serilog": { "Using": [ "Serilog.Sinks.Console" ], "MinimumLevel": { "Default": "Debug", "Override": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "WriteTo": [ { "Name": "Logger", "Args": { "configureLogger": { "Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "(@l='Error' or @l='Fatal' or @l='Warning')" } } ], "WriteTo": [ { "Name": "File", "Args": { "path": "C:\\CatiaWidgets\\Logs\\WarnErrFatal_.log", "outputTemplate": "{Timestamp:o} [{Level:u3}] ({SourceContext}) {Message}{NewLine}{Exception}", "rollingInterval": "Day", "retainedFileCountLimit": 7 } } ] } } }, { "Name": "Logger", "Args": { "configureLogger": { "Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "(@l='Information' or @l='Debug')" } } ], "WriteTo": [ { "Name": "File", "Args": { "path": "C:\\CatiaWidgets\\Logs\\DebugInfo_.log", "outputTemplate": "{Timestamp:o} [{Level:u3}] ({SourceContext}) {Message}{NewLine}{Exception}", "rollingInterval": "Day", "retainedFileCountLimit": 7 } } ] } } } ], "Enrich": [ "FromLogContext", "WithMachineName" ], "Properties": { "Application": "MultipleLogFilesSample" } } }
MainWindow.xaml
<Window x:Class="TestApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:views="clr-namespace:TestApp.Views" xmlns:viewmodels="clr-namespace:TestApp.ViewModels" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid Grid.Column="1" Grid.Row="1" Grid.RowSpan="2" Background="#151f2d" Margin="0 0 0 0"> <Grid.Resources> <DataTemplate DataType="{x:Type viewmodels:LeftViewModel}"> <views:LeftView/> </DataTemplate> <DataTemplate DataType="{x:Type viewmodels:RightViewModel}"> <views:RightView/> </DataTemplate> </Grid.Resources> <ContentControl Content="{Binding CurrentViewModel}"/> </Grid> </Window>
MainWindow.xaml.cs
using System.Windows; namespace TestApp { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } }