Last updated on August 4, 2022
There is an update (.NET 6) version of this available at: https://codingwithscott.com/how-to-implement-the-publish-subscribe-design-pattern-in-c/
Video version here:
Where to use it
In many programs, one object needs to know when something happens to another object.
One way to do this is to have the first object check the second object – usually by getting a value from one of the second object’s properties. However, there are two problems with this technique.
First, the first object needs to check the second object every time it does something that affects the second object. This leads to a lot of duplicated code – which is easy to forget to do every time.
Second, a third object might change the second object – and the first object would not know about the change.
A way to avoid these problems is to use the publish/subscribe pattern.
The first object can “subscribe” to an event on the second object. When the second object has a value change, it can “publish” an event to any objects that are subscribed.
This way, the first object will always know about the event – no matter what object, or function, causes the event to happen.
For this demonstration, we’ll use classes from a role-playing game. A GameSession object, which manages how the user input works with the other game objects (like a ViewModel, in MVVM). It needs to watch the Player object, to see if the player’s hit points are ever zero, or lower.
Non-Pattern Version
The GameSession class has two functions that subtract hit points from the player. The first, when a monster attacks the player. The second, if the player moves to a location with a poisonous atmosphere.
Without using the publish/subscribe design pattern, each of those functions needs to check if the player has zero hit points. If we add more functions that subtract hit points from the player, we would need to add this same check in each function.
Location.cs
namespace Engine.PublishSubscribePattern.NonPatternVersion.Models { public class Location { public enum AtmosphereType { Normal, Poisonous } public AtmosphereType Atmosphere { get; set; } } }
Player.cs
namespace Engine.PublishSubscribePattern.NonPatternVersion.Models { public class Player { public int HitPoints { get; set; } } }
GameSession.cs
using System.Collections.Generic; using Engine.PublishSubscribePattern.NonPatternVersion.Models; namespace Engine.PublishSubscribePattern.NonPatternVersion.ViewModels { public class GameSession { public List<string> Messages { get; set; } public Player CurrentPlayer { get; set; } public Location CurrentLocation { get; set; } public GameSession() { Messages = new List<string>(); CurrentPlayer = new Player {HitPoints = 10}; CurrentLocation = new Location {Atmosphere = Location.AtmosphereType.Normal}; } public void MonsterAttackPlayer(int amountOfDamage) { CurrentPlayer.HitPoints -= amountOfDamage; CheckIfPlayerWasKilled(); } public void MoveToLocation(Location location) { CurrentLocation = location; if(CurrentLocation.Atmosphere == Location.AtmosphereType.Poisonous) { CurrentPlayer.HitPoints -= 1; CheckIfPlayerWasKilled(); } } private void CheckIfPlayerWasKilled() { if(CurrentPlayer.HitPoints <= 0) { Messages.Add("You were killed"); } } } }
Pattern Version
With the publish/subscribe design pattern, we add an “event” to the Player class. Other objects can “subscribe” to this event.
If the player’s hit points ever reach zero, the player object “publishes” a message to any objects subscribed to the PlayerKilled EventHandler.
The GameSession object only needs to subscribe to this EventHandler once. If the player ever reaches zero hit points, from any part of the program, the player object will publish the message.
If we add new functions, where the player will receive damage, we don’t need to have those functions check for zero hit points. The GameSession object will automatically be notified from the Player object.
This makes it much easier to add new game features and capabilities.
Location.cs
namespace Engine.PublishSubscribePattern.PatternVersion.Models { public class Location { public enum AtmosphereType { Normal, Poisonous } public AtmosphereType Atmosphere { get; set; } } }
Player.cs
using System; namespace Engine.PublishSubscribePattern.PatternVersion.Models { public class Player { private int _hitPoints; public EventHandler PlayerKilled; public int HitPoints { get { return _hitPoints; } set { _hitPoints = value; if(_hitPoints <= 0) { // When the player's HitPoint property is zero, or lower, // the player object will raise a PlayerKilled notification to all subscribed objects. OnPlayerKilled(); } } } private void OnPlayerKilled() { // If there are no subscribed objects, // the PlayerKilled EventHandler will be null, // and nothing will be notified of this event. PlayerKilled?.Invoke(this, EventArgs.Empty); } } }
GameSession.cs
using System; using System.Collections.Generic; using Engine.PublishSubscribePattern.PatternVersion.Models; namespace Engine.PublishSubscribePattern.PatternVersion.ViewModels { public class GameSession { public List<string> Messages { get; set; } public Player CurrentPlayer { get; set; } public Location CurrentLocation { get; set; } public GameSession() { Messages = new List<string>(); CurrentPlayer = new Player {HitPoints = 10}; CurrentLocation = new Location {Atmosphere = Location.AtmosphereType.Normal}; // "Subscribe" to the PlayerKilled event. // // When the GameSession object receives this notification, // it will run the HandlePlayerKilled function. CurrentPlayer.PlayerKilled += HandlePlayerKilled; } public void MonsterAttackPlayer(int amountOfDamage) { CurrentPlayer.HitPoints -= amountOfDamage; } public void MoveToLocation(Location location) { CurrentLocation = location; if(CurrentLocation.Atmosphere == Location.AtmosphereType.Poisonous) { CurrentPlayer.HitPoints -= 1; } } private void HandlePlayerKilled(object sender, EventArgs eventArgs) { Messages.Add("You were killed"); } } }
Pattern Version (with “event”)
When you define the PlayerKilled EventHandler, you can also declare it as an event. This is a more common way to do it.
In the Player.cs class, when you change this line:
public EventHandler PlayerKilled;
to this:
public event EventHandler PlayerKilled;
IntelliSense will recognize that the PlayerKilled EventHandler is an event. So, it will show a lightning bolt when you hover over it, like this:
instead of showing it only as a field, like this:
Pattern Version (with custom EventArgs)
You might want to include additional information with your events. To do that, you can create a custom EventArgs class.
Your custom EventArgs class must inherit from the base EventArgs class. Then, you can add whatever properties you want, to hold the additional information.
In this example, the PlayerKilledEventArgs class has a constructor that takes the number of deaths and populates a property with that value.
In the Player class (the publisher), we change the EventHandler to include the type of EventArgs object it will pass – our new PlayerKilledEventArgs object. Then, in the OnPlayerKilled function, we create a new instance of the PlayerKilledEventArgs object, passing in the number of deaths parameter.
In the GameSession class (the subscriber), we change the HandlePlayerKilled function to accept the PlayerKilledEventArgs object. Then, we can add a new line that displays the number of deaths in the list of messages.
Location.cs
namespace Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Models { public class Location { public enum AtmosphereType { Normal, Poisonous } public AtmosphereType Atmosphere { get; set; } } }
PlayerKilledEventArgs.cs
using System; namespace Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Common { public class PlayerKilledEventArgs : EventArgs { public int NumberOfDeaths { get; private set; } public PlayerKilledEventArgs(int numberOfDeaths) { NumberOfDeaths = numberOfDeaths; } } }
Player.cs
using System; using Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Common; namespace Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Models { public class Player { private int _hitPoints; private int _numberOfDeaths; public EventHandler<PlayerKilledEventArgs> PlayerKilled; public int HitPoints { get { return _hitPoints; } set { _hitPoints = value; if(_hitPoints <= 0) { // Increase the "number of deaths" counter. _numberOfDeaths++; // When the player's HitPoint property is zero, or lower, // the player object will raise a PlayerKilled notification to all subscribed objects. OnPlayerKilled(); } } } protected virtual void OnPlayerKilled() { PlayerKilled?.Invoke(this, new PlayerKilledEventArgs(_numberOfDeaths)); } } }
GameSession.cs
using System.Collections.Generic; using Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Common; using Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.Models; namespace Engine.PublishSubscribePattern.PatternVersion_CustomEventArgs.ViewModels { public class GameSession { public List<string> Messages { get; set; } public Player CurrentPlayer { get; set; } public Location CurrentLocation { get; set; } public GameSession() { Messages = new List<string>(); CurrentPlayer = new Player {HitPoints = 10}; CurrentLocation = new Location {Atmosphere = Location.AtmosphereType.Normal}; // "Subscribe" to the PlayerKilled event. // // When the GameSession object receives this notification, // it will run the HandlePlayerKilled function. CurrentPlayer.PlayerKilled += HandlePlayerKilled; } public void MonsterAttackPlayer(int amountOfDamage) { CurrentPlayer.HitPoints -= amountOfDamage; } public void MoveToLocation(Location location) { CurrentLocation = location; if(CurrentLocation.Atmosphere == Location.AtmosphereType.Poisonous) { CurrentPlayer.HitPoints -= 1; } } private void HandlePlayerKilled(object sender, PlayerKilledEventArgs eventArgs) { Messages.Add("You were killed"); Messages.Add($"This was death number: {eventArgs.NumberOfDeaths}"); } } }
What to watch out for
You should be aware of possible problems with multi-threaded programs.
If you are using C# 6.0, the code in OnPlayerKilled (in the Pattern Version – with custom EventArgs) is thread-safe. So, if an object subscribes to the event from another thread, and later un-subscribes from it, the program will not throw an exception when the OnPlayerKilled function tries to call Invoke on the PlayerKilled EventHandler.
If you want more details, please see this post on Jon Skeet’s site. That post also shows how to raise events in a thread-safe way, in previous versions of C#.
Source code for my design pattern lessons
Hello Scott – thank you again for doing the video and helping us learn and understand c# better.
Since you ask for suggestions for topics – I have a question.
What in your opinion is the best strategy for Exception logging in a project that involves multiple business objects some of which also connect to a database.
Would it make sense to handle it centrally by throwing exceptions (while preserving the callstack by using the ExceptionDispatchInfo object or something similar) after handling local operations like disposing unmanaged objects, closing connections etc. and letting the central code log exceptions, OR
if we handle it onsite what is a good strategy to bubble up the chain, the fact that –
A. there was an exception down the chain and
B. the fact that the exception was actually handled and logged.
Thank you.
You’re welcome.
Error-handling strategies is a good idea for a future post (or maybe two or three – there is a lot to cover). For now, a couple of quick comments on how I usually handle errors:
1. I normally let the error bubble up to the top, and let the top level of the program handle the display/logging of the error.
2. To ensure disposable objects are properly disposed, I use a “using” block (more details here: http://stackoverflow.com/questions/4717789/in-a-using-block-is-a-sqlconnection-closed-on-return-or-exception).
***Not sure if I replied already, please ignore if I already did!***
Thanks Scott.
Yes I’ve wrapped my DB code in using statements, I was referring to closing/disposing connections as an example of local cleanup needed prior to bubbling up the exception for logging.
In my code I’m currently using the logging strategy that you outlined above – handle clean up locally and then handle logging centrally.
Please consider doing a video/article on different strategies for exception handling/logging.
Thank you.
Exception handling, and logging, is now on the list. Thanks for the suggestion!
Thank you Scott for the well structured and detailed videos.
They did not let questions unanswered.
It wluld be great if you could cover also Repository and UnitOfWork.
Thanks again!
You’re welcome! I’ve added Repository and Unit of Work to my “To Do” list. I’ve been thinking about it, and haven’t come up with small example (so the video can focus on the principle, and not a large amount of code). At least, I not yet.
Hi Scott, can you tell me what difference it will make if I use a Action callback instead of Event. As I use it very often.
Hello Vinod, I have not used Action callbacks with this pattern. It seems like a good idea for event-handling code that needs to perform the same type of function, but can be slightly changed with a parameter. I want to try this in some of my code. Have you seen any problems with using them?