Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Event Communication using .NET Framework Event and Prism's Event Aggregator

4.43/5 (8 votes)
20 Aug 2016CPOL4 min read 13.6K   147  
A look at both tightly and loosely coupled components design.

Download Demo

1. Background

When work on an application, for example, one that follows MVVM pattern, we will need a mechanism to communicate between view-models (VMs). For example, a master VM containing a list of all employees, and a detailed VM of currently selected employee. We need to notify the detailed VM when the current selected employee is changed from the master VM. Also, the master VM needs to know when the detailed VM has updated its selected employee (such as changing the employee's name, or deleting that employee) so that the master VM can update its list of all employees.

While different traditional techniques of using .NET delegate/event can do the job. Their disadvantages encourage using a new approach: event aggregator. This article is to re-visit the 'standard' .NET event-driven programming technique, then move to a a better maintainable and testable way to communicating between loosely coupled components that the Prism library has to offer.

The example here is about the government that is interested in the income event of a person. The government (acts as the subscriber), the person (acts as the publisher), and the event of interest is when the person's total income exceeds 30 USD.

Let's get started with traditional .NET Event.

2. Using .NET Framework Event

Some key points to remember before going further:

  • The publisher determines when to raise the event.
  • The subscriber determines how to handle the event: what to do when the event is raised.
  • Multiple subscribers can subscribe to an event of a publisher. In this case, the event handlers are invoked in the order corresponding to the order of the raising events.
  • Also, a subscriber can subscribe to multiple events from multiple publishers.

We're going to use the generic version of delegate EventHandler<TEventArgs>. Its signature is:

C#
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)

Here, the sender is the source of the event. That means it is the publisher instant object that raises the event, represented by the 'this' keyword.

These are the steps to follow:

2.1. Implement the Publisher

C#
public class Person
{
    // Constructors, properties,...

    double _income;

    public void MakeMoney(double money)
    {
        _income += money;

        // When total income becomes greater than 30, then raise the event 'IncomeReached'
        // with related information encapsulated in an 'IncomeReachedEventArg' object.
        if (_income > 30)
        {
            var incomeReachedEventArg = new IncomeReachedEventArg
            {
                TotalIncome = _income,
                IncomeReportedDate = DateTime.Now
            };

            OnIncomeReached(incomeReachedEventArg);
        }
    }

    protected virtual void OnIncomeReached(IncomeReachedEventArg e)
    {
        IncomeReached?.Invoke(this, e);
    }

    public event EventHandler<IncomeReachedEventArg> IncomeReached;
}

Where IncomeReachedEventArg is TEventArgs. We can encapsulate as much necessary data into it, to pass to the subscribers' event handler:

C#
public class IncomeReachedEventArg
{
    public double TotalIncome { get; set; }
    public DateTime IncomeReportedDate { get; set; }
    // ... more if needed
}

Based on the convention, usually this event data IncomeReachedEventArg derives from the EventArgs base class. Although it's OK not to.

2.2. Implement the Subscriber

C#
public class Government
{
    // Constructors and properties omitted...

    // What to do in response to the event
    public void Person_IncomeReached(object sender, IncomeReachedEventArg e)
    {
        // Logic to determine amount of tax based on other factors, such as income, age, etc.
        WriteLine($"On {e.IncomeReportedDate.ToString("d")}, {((Person)sender).Name} has reached the total taxable income of {e.TotalIncome} USD.");
    }
}

Here, the publisher's event handler Person_IncomeReached must satisfy the signature of the delegate EventHandler<TEventArgs> where TEventArgs is IncomeReachedEventArg.

2.3. Subscribe to the event, and raise the event

C#
static void Main()
{
    WriteLine("\t.Demo 1: using .NET Event\n");

    Person person = new Person("John Doe"); // publisher

    Government gov = new Government(); // subscriber

    // Subscribe to the event 
    // (here, one or multiple subscribers can subscribe/un-subscribe to the event)
    // In this case, the government object subscribes to the 'IncomeReached' of the person object
    person.IncomeReached += gov.Person_IncomeReached;

    // Now, try to raise the event
    person.MakeMoney(15);
    person.MakeMoney(16); // Condition met, fire the event
    person.MakeMoney(-10);
    person.MakeMoney(20); // Condition met, fire the event

    ReadLine();
}

Run the application, this is the result:
Image 1

Complete code for this .NET Framework Event solution

C#
namespace DotNetEvent
{
    class Program
    {
        static void Main()
        {
            WriteLine("\t.Demo 1: using .NET Event\n");

            Person person = new Person("John Doe"); // publisher

            Government gov = new Government(); // subscriber

            // Subscribe to the event
            person.IncomeReached += gov.Person_IncomeReached;

            person.MakeMoney(15);
            person.MakeMoney(16); // Condition met, fire the event
            person.MakeMoney(-10);
            person.MakeMoney(20); // Condition met, fire the event

            ReadLine();
        }
    }

    // Event Publisher
    public class Person
    {
        double _income;
        string _personName;

        public Person(string personName)
        {
            _personName = personName;
        }

        public string Name
        {
            get { return _personName; }
        }
        
        public void MakeMoney(double money)
        {
            _income += money;

            // When total income becomes greater than 30, then raise the event 'IncomeReached'
            // with related information encapsulated in an 'IncomeReachedEventArg' object.
            if (_income > 30)
            {
                var incomeReachedEventArg = new IncomeReachedEventArg
                {
                    TotalIncome = _income,
                    IncomeReportedDate = DateTime.Now
                };

                OnIncomeReached(incomeReachedEventArg);
            }
        }

        protected virtual void OnIncomeReached(IncomeReachedEventArg e)
        {
            IncomeReached?.Invoke(this, e);
        }

        public event EventHandler<IncomeReachedEventArg> IncomeReached;
    }

    public class IncomeReachedEventArg
    {
        public double TotalIncome { get; set; }
        public DateTime IncomeReportedDate { get; set; }
    }

    // Event Subscriber
    public class Government
    {
        // Constructors and properties omitted...

        // What to do in response to the event
        public void Person_IncomeReached(object sender, IncomeReachedEventArg e)
        {
            // Logic to determine amount of tax based on other factors, such as income, age, etc.
            WriteLine($"On {e.IncomeReportedDate.ToString("d")}, {((Person)sender).Name} has reached the total taxable income of {e.TotalIncome} USD.");
        }
    }
}

Notes about the .NET Framework Event approach

The subscriber (government object) needs a reference to the publisher (person object) to wire the publisher's event (IncomeReached) to the subscriber's method handler (Person_IncomeReached). In other word, the government object needs to know and subscribe to, say, many person objects: too much of work! Will it be easier if both the government and the person don't need to have knowledge about each other? (Yes, and the Prism's Event Aggregator will help that).

That (the above solution) leads to a tightly coupled design, which is hard to unit test and maintain. Also, memory leak is a possible to happen. For example, what if the government object get garbage collected without un-subscribing, then the person object cannot get garbage collected neither.

Now, let's go to the Event Aggregator section.

3. Using Prism's Event Aggregator

The purpose of the Prism Event Aggregator is to decouple the publisher from subscriber. In other word, it enables communications between loosely coupled components of the application. The publishers and subscribers can communicate (send and receive events) and still do not need to maintain references to each other.

Image 2
Image from Magnus Montin .NET blog.

These are the steps to follow:

3.1 Add NuGet Prism Package

Since the Prism.PubSubEvents is obsolete, use the new Prism.Core (6.2.0) instead:
Image 3

3.2. Create the Event

Our custom event, named IncomeReachedEvent, derives from the PubSubEvent<TPayload> base class in the Prism.Events namespace, where TPayload is the message that will be passed to the subscribers. Similar to EventArgs, we can encapsulate as much necessary data into it (IncomeMessage):

C#
public class IncomeReachedEvent : PubSubEvent<IncomeMessage> { }

public class IncomeMessage
{
    public string PersonName { get; set; }
    public double TotalIncome { get; set; }
    public DateTime IncomeReportedDate { get; set; }
    ... more related data if needed
}

The PubSubEvent<TPayload> class is the most important one to connect publishers and subscribers, because it does all the work of subscribing/un-subscribing, publishing, and more.

3.3. Implement the Publisher

C#
public class Person
{
    double _income;
    string _personName;
    IEventAggregator _eventAggregator;

    public Person(string personName, EventAggregator eventAggregator)
    {
        this._personName = personName;
        this._eventAggregator = eventAggregator;
    }
        
    public void MakeMoney(double money)
    {
        _income += money;

        // When total income becomes greater than 30, then fire the event 'IncomeReached'
        // with related information encapsulated in an 'IncomeReachedEventArg' object.
        if (_income > 30)
        {
            var message = new IncomeMessage
            {
                PersonName = _personName,
                TotalIncome = _income,
                IncomeReportedDate = DateTime.Now
            };

            _eventAggregator.GetEvent<IncomeReachedEvent>().Publish(message);
        }
    }
}

As above, the person (publisher) raises the event by retrieving the event (of type IncomeReachedEvent) from the IEventAggregator object, and calls the Publish(TPayload payload) method where payload is our custom message object (IncomeMessage):

C#
_eventAggregator.GetEvent<IncomeReachedEvent>().Publish(message);

3.4. Implement the Subscriber

There are several overloads of subscribing methods provided by the PubSubEvent class, depending on different situations. For example, whether or not to update UI, to filter an event, or have performance concern. For more info, visit MSDN Prism 5 Guide (version 5, old but still relevant and useful while we're waiting for version 6 documentation). Here, the default subscription is used:

C#
public class Government
{
    // Constructor, properties,...

    // Method or property setter that does the subscription
    private void Init()
    {
        // Subscribe to the interested event, and handle when it happens
        var incomeReachedEvent = _eventAggregator.GetEvent<IncomeReachedEvent>();

        incomeReachedEvent.Subscribe(message =>
        {
            WriteLine($"On {message.IncomeReportedDate.ToString("d")}, {message.PersonName} has reached the total taxable income of {message.TotalIncome} USD.");
        });
    }
}

3.5. Raise the Event

C#
static void Main()
{
    WriteLine("\t.Demo 2: using Prism's Event Aggregator\n");

    var evt = new EventAggregator();
    var person = new Person("John Doe", evt);
    var gov = new Government(evt);

    person.MakeMoney(15);
    person.MakeMoney(16); // Condition met, fire the event
    person.MakeMoney(-10);
    person.MakeMoney(20); // Condition met, fire the event

    ReadLine();
}

The result is:
Image 4

Complete code for the Prism's Event Aggregator solution

C#
namespace DotNetPrismEventAggregator
{
    class Program
    {
        static void Main()
        {
            WriteLine("\t.Demo 2: using Prism's Event Aggregator\n");

            var evt = new EventAggregator();
            var person = new Person("John Doe", evt);
            var gov = new Government(evt);

            person.MakeMoney(15);
            person.MakeMoney(16); // Condition met, fire the event
            person.MakeMoney(-10);
            person.MakeMoney(20); // Condition met, fire the event

            ReadLine();
        }
    }

    // Event Publisher
    public class Person
    {
        double _income;
        string _personName;
        IEventAggregator _eventAggregator;

        public Person(string personName, EventAggregator eventAggregator)
        {
            this._personName = personName;
            this._eventAggregator = eventAggregator;
        }
        
        public void MakeMoney(double money)
        {
            _income += money;

            // When total income becomes greater than 30, then fire the event 'IncomeReached'
            // with related information encapsulated in an 'IncomeReachedEventArg' object.
            if (_income > 30)
            {
                var message = new IncomeMessage
                {
                    PersonName = _personName,
                    TotalIncome = _income,
                    IncomeReportedDate = DateTime.Now
                };

                _eventAggregator.GetEvent<IncomeReachedEvent>().Publish(message);
            }
        }
    }

    public class IncomeReachedEvent : PubSubEvent<IncomeMessage> { }

    public class IncomeMessage
    {
        public string PersonName { get; set; }
        public double TotalIncome { get; set; }
        public DateTime IncomeReportedDate { get; set; }
    }

    // Event Subscriber
    public class Government
    {
        IEventAggregator _eventAggregator;
        public Government(EventAggregator eventAggregator)
        {
            _eventAggregator = eventAggregator;

            Init(); // Do some necessary things
        }

        private void Init()
        {
            // Subscribe to the interested event, and handle when it happens
            var incomeReachedEvent = _eventAggregator.GetEvent<IncomeReachedEvent>();

            incomeReachedEvent.Subscribe(message =>
            {
                WriteLine($"On {message.IncomeReportedDate.ToString("d")}, {message.PersonName} has reached the total taxable income of {message.TotalIncome} USD.");
            });
        }
    }
}

Conclusion

As we saw, the Prism's Event Aggregator is so useful. We can use it now, or wait for official documentation and books to learn more./.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)