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:
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
public class Person
{
double _income;
public void MakeMoney(double money)
{
_income += money;
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:
public class IncomeReachedEventArg
{
public double TotalIncome { get; set; }
public DateTime IncomeReportedDate { get; set; }
}
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
public class Government
{
public void Person_IncomeReached(object sender, IncomeReachedEventArg e)
{
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
static void Main()
{
WriteLine("\t.Demo 1: using .NET Event\n");
Person person = new Person("John Doe");
Government gov = new Government();
person.IncomeReached += gov.Person_IncomeReached;
person.MakeMoney(15);
person.MakeMoney(16);
person.MakeMoney(-10);
person.MakeMoney(20);
ReadLine();
}
Run the application, this is the result:
Complete code for this .NET Framework Event solution
namespace DotNetEvent
{
class Program
{
static void Main()
{
WriteLine("\t.Demo 1: using .NET Event\n");
Person person = new Person("John Doe");
Government gov = new Government();
person.IncomeReached += gov.Person_IncomeReached;
person.MakeMoney(15);
person.MakeMoney(16);
person.MakeMoney(-10);
person.MakeMoney(20);
ReadLine();
}
}
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;
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; }
}
public class Government
{
public void Person_IncomeReached(object sender, IncomeReachedEventArg e)
{
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 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:
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):
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
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;
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):
_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:
public class Government
{
private void Init()
{
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
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);
person.MakeMoney(-10);
person.MakeMoney(20);
ReadLine();
}
The result is:
Complete code for the Prism's Event Aggregator solution
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);
person.MakeMoney(-10);
person.MakeMoney(20);
ReadLine();
}
}
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;
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; }
}
public class Government
{
IEventAggregator _eventAggregator;
public Government(EventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
Init();
}
private void Init()
{
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./.