Tuesday, January 23, 2024

Event Aggregator

I found a good article about basic event aggregation on Stack Overflow and decided to create a demonstration that runs on WPF using MVVM. The original article was written by Mike Hankey.

https://www.codeproject.com/Articles/5376132/EventAggregator-My-Take

I recommend you read his article first. I think it needs an unsubscribe method otherwise references to the handlers will prevent objects being removed from memory. Here's my fully working WPF version.

Use Visual Studio to create a C# WPF Core project called EventAggregator. We will start by creating some base classes.

Add a class called NotifyWindow. It inherits Window and adds NotifyPropertyChanged functionality.

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace EventAggregator
{
    public class NotifyWindow : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

Add a class called EventAggregator. This is taken directly from Mike's code. I added my own Unsubscribe method.

namespace EventAggregator
{
    public class EventAggregator<T>
    {
        private readonly Dictionary<Type, List<Delegate>> subscribers = new Dictionary<Type, List<Delegate>>();

        public void Subscribe<TEvent>(Action<TEvent> handler)
        {
            Type eventType = typeof(TEvent);
            if (!subscribers.TryGetValue(eventType, out var handlers))
            {
                handlers = new List<Delegate>();
                subscribers[eventType] = handlers;
            }

            handlers.Add(handler);
        }

        public bool Unsubscribe<TEvent>(Action<TEvent> handler)
        {
            if (subscribers.TryGetValue(typeof(TEvent), out var handlers))
            {
                return handlers.Remove(handler);
            }
            return false;
        }

        public void Publish<TEvent>(TEvent message)
        {
            Type eventType = typeof(TEvent);
            if (subscribers.TryGetValue(eventType, out var handlers))
            {
                foreach (Action<TEvent> handler in handlers)
                {
                    handler.Invoke(message);
                }
            }
        }
    }
}

The EventAggregator class is very generic and can handle messages of any type as long as the type implements IEvent. In this example we define a message of type TestMessage. I have simplified Mike's TestMessage class (and made it less useful - you should probably stay with his class). Add a class called EventArgs.

namespace EventAggregator
{
    public interface IEventArgs<T>
    {
        T GetData();
    }

    public class Test
    {
        public class TestMessage : IEventArgs<String>
        {
            String _Message = "";

            public TestMessage(string s)
            {
                _Message = s;
            }

            public String GetData()
            {
                return _Message;
            }
        }
    }
}

The next step is to create an instance of the aggregator. In this example I have declared it in the App class. In most scenarios it will be accessible through dependency injection. Alter App.xaml.cs to look like this.

using System.Windows;

namespace EventAggregator
{
    public partial class App : Application
    {
        public static EventAggregator<IEventArgs<Object>> g_Aggregator = new EventAggregator<IEventArgs<Object>>();
    }
}

The application will display a main window and a dialog window. The two windows will communicate through a publish/subscribe model. Add a Window called DialogWindow. The xaml and C# look like this. Changing the value of DialogMessage causes all subscribers to be notified.

<local:NotifyWindow x:Class="EventAggregator.DialogWindow"
        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:local="clr-namespace:EventAggregator"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="DialogWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical" HorizontalAlignment="Left">
        <TextBlock Text="Please enter a message for the main window"/>
        <TextBox Text="{Binding DialogMessage, UpdateSourceTrigger=PropertyChanged}" Width="200" BorderBrush="Gray" BorderThickness="1"/>
    </StackPanel>
</local:NotifyWindow>


namespace EventAggregator
{
    public partial class DialogWindow : NotifyWindow
    {
        private string _DialogMessage = "";
        public string DialogMessage
        {
            get => _DialogMessage;
            set
            {
                SetProperty(ref _DialogMessage, value);
                App.g_Aggregator.Publish(new Test.TestMessage(DialogMessage));
            }
        }

        public DialogWindow()
        {
            InitializeComponent();
        }
    }
}

Finally, here is the MainWindow which subscribes to the TestMessage publications.

<local:NotifyWindow x:Class="EventAggregator.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:local="clr-namespace:EventAggregator"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <TextBlock Text="A message from the dialog window"/>
        <TextBlock Text="{Binding DialogMessage}"/>
    </StackPanel>
</local:NotifyWindow>


namespace EventAggregator
{
    public partial class MainWindow : NotifyWindow
    {
        private string _DialogMessage = "";
        public string DialogMessage
        {
            get => _DialogMessage;
            set => SetProperty(ref _DialogMessage, value);
        }

        public MainWindow()
        {
            this.Unloaded += MainWindow_Unloaded;
            InitializeComponent();

            DialogWindow dw = new DialogWindow();
            App.g_Aggregator.Subscribe<Test.TestMessage>(HandleTestMessage);

            dw.Show();
        }

        private void MainWindow_Unloaded(object sender, System.Windows.RoutedEventArgs e)
        {
            App.g_Aggregator.Unsubscribe<Test.TestMessage>(HandleTestMessage);
        }

        private void HandleTestMessage(Test.TestMessage message)
        {
            DialogMessage = message.GetData();
        }
    }
}

When you run this application you see two windows. Anything typed into the dialog window is updated in the main window through the event aggregator.





No comments:

Post a Comment