One of the coolest features of C# is a type of member called an event . An event is a way for one object to tell other objects that some important thing has happened. Objects that wish to be informed can subscribe to the event to get notified.
An event is just another type of member, like a field, a method, a constructor, or a property. You can add events to classes and structs, and you can include them in an interface.
Consider this class that defines a ship:
public class Ship
{
public int RemainingHealth { get; private set; }
public Point Location { get; set; }
public Ship()
{
RemainingHealth = 20; // Start everything at 20.
}
public void DealDamage(int amount)
{
// If this ship is already destroyed, don't do anything.
if (RemainingHealth == 0) return;
// Reduce the health by the desired amount.
RemainingHealth -= amount;
// Prevent ourselves from dropping below zero.
if(RemainingHealth < 0)
RemainingHealth = 0;
}
}
This is a pretty reasonable beginning to this Ship
class, but a ship being destroyed is likely an important event that others might like to be aware of.
For example, the audio system might want to play an explosion sound. The score keeping system might want to add points to the other player. A particle effect system might want to add a new explosion effect. Another component in the game that is watching all still-surviving ships may want to stop paying attention to the destroyed ship. The possibilities are endless.
note
It is possible for Ship
to directly kick off all these things upon destruction:
SoundEffects.Play("ShipExplosion");
Players.FindOpponentFor(this).Score += 10;
ParticleEffects.Begin("ExplosionEffect");
// ...
But that would mean Ship
needs to be aware of lots and lots of other things in the system.
Ship
can simply call a method on another object if it is aware of its existence, but the event paradigm lets other objects of all sorts of crazy types respond to the event without Ship
ever needing to know they even exist.
The first step is to add an event to this Ship
class.
This is just another member, like any other field, property, constructor, or method, and the order doesn’t matter.
But the following can be added anywhere inside of the Ship
class:
public event Action Destroyed;
This declares an event by using the event
keyword.
After that comes the event’s type, which is Action
, in this case.
An event’s type must be some sort of delegate type.
As we saw in the previous tutorial, you can declare your own delegate types, but you often can just use one of the existing ones.
That’s what I chose to do here, using the Action
delegate type, which is for void
-returning methods with no parameters.
Once the event exists, the next step is to raise the event. This begins the process of informing any listeners or subscribers that the event has occurred.
First, we must identify the place where our code can tell that the event has occurred.
In our specific situation, that is in the DealDamage
method, but only if the remaining health is zero.
We can raise the event as shown below:
public void DealDamage(int amount)
{
// If this ship is already destroyed, don't do anything.
if (RemainingHealth == 0) return;
// Reduce the health by the desired amount.
RemainingHealth -= amount;
// Prevent ourselves from dropping below zero.
if (RemainingHealth < 0)
RemainingHealth = 0;
if (RemainingHealth == 0) // The ship has been destroyed!
Destroyed(); // Invoke the event.
}
That Destroyed()
line invokes or raises the event, notifying all subscribed objects.
Events are built on delegates, so this is a delegate object.
Which means, if you prefer, you could also call it this way: Destroyed.Invoke();
.
If there are no subscribers, the delegate will be empty, and contain a special value called null
.
Bad things happen if you try to call a method on null
, because there is nothing there to call it on!
Thus, it is a good idea to check your event for null first, like this:
if (Destroyed != null)
Destroyed.Invoke();
In fact, you can do even better.
C# has a special operator to make this type of null checking simple: the ?.
operator. Instead of the above, you can do:
Destroyed?.Invoke();
This is the equivalent of performing a check to see if Destroyed
is null, and if it is not null, it proceeds to call Invoke
.
We’ve only seen one side of the picture so far. When another object wants to know about an event, it needs to subscribe to it.
Imagine you have ScoringSystem
that pays attention to a number of events from different places and updates player scores.
(This is, of course, far from the only way you can deal with scoring in a game. Think of this as a possibility, not a recommendation for how to do it.)
Ship ship = GetTheShip(); // Not important right now how this class finds the ship in the first place.
ship.Destroyed += OnShipDestroyed;
Subscribing is done through the +=
operator.
The above code tells the ship, “When this ship is destroyed, call my method named OnShipDestroyed
.”
Of course, we have to define that method:
private void OnShipDestroyed()
{
_player.Score += 10;
}
In the method, you will place whatever code you need to respond to the event. In this case, we’ve given the player 10 points.
It is important to point out that the method you subscribe with must match the delegate type of the event.
Since this event used Action
, which indicates a void
-returning method with no parameters, OnShipDestroyed
had to work the same way.
If an object decides is no longer needs to see an event, it can unsubscribe with the -=
operator:
ship.Destroyed -= OnShipDestroyed;
Afterward, it will no longer be notified that the event has occurred.
The Action
delegate type we used before is about as simple as it comes.
It is often useful to be able to relay information to subscribers when the event occurs.
By using a different delegate type, you can pass that information over as a parameter.
For example, we could change the event to include the location of the ship, so subscribers can use that as a part of their event handling code. (The sound effect manager may choose to play sound out of the left or right speaker depending on the location, as an example.)
We can start by changing the event’s delegate type to Action<Point>
instead of just Action
:
public event Action<Point> Destroyed;
When we raise the event, we’ll need to raise it with the information we care about:
Destroyed?.Invoke(ship.Location);
And then when subscribing, we must subscribe with a method with a single Point
parameter, which allows us to use it in the event handler:
private void OnShipDestroyed(Point location)
{
Console.WriteLine("The ship was destroyed at " + location.X + " " + location.Y);
}
EventHandler
TypeWhile we’re on the topic of other delegate types for events, one popular choice is the EventHandler
delegate type, which is a void
-returning method with two parameters: an object
, which represents the source of the event and an EventArgs
object.
We could make our event look like this:
public event EventHandler Destroyed;
Raise the event like this:
Destroyed?.Invoke(this, EventArgs.Empty);
And our handler could look like this:
private void OnShipDestroyed(object source, EventArgs args)
{
Ship ship = (Ship)source;
Console.WriteLine("The ship was destroyed at " + ship.Location.X + " " + ship.Location.Y);
}
There is also a generic version of EventHandler
, which lets you specify another type for the EventArgs
object:
public event EventHandler<GameEventArgs> Destroyed;
This requires you to declare the event args type (it must be derived from EventArgs
), perhaps as something like this:
class GameEventArgs : EventArgs
{
public Game Game { get; } // We haven't ever defined a `Game` class, so pretend it exists.
public GameEventArgs(Game game)
{
Game = game;
}
}
We can then raise the event like this:
Destroyed?.Invoke(this, new GameEventArgs(_game)); // Assume we have access to a `Game` object through a `_game` field somehow.
This passes both the source object (this
, which is a Ship
, in this case) and any other arguments you may have packed into the specific event args object.
That allows the handler to use it like this:
private void OnShipDestroyed(object source, GameEventArgs args)
{
if (args.Game.IsOver)
Console.WriteLine("Game over.");
}
EventHandler
and EventHandler<T>
are both common choices for event types, but so is all of the different flavors of Action
.
And, of course, you can declare your own delegate type.
Events can work with any delegate type.