Events are of utter importance when making applications. One cannot assume that all actions of an application happen sequential. When an application runs, the user can press a button and some actions will happen accordingly. In .NET, this is done via events. When the user presses a button, all objects that have subscribed on that event are warned that an action should be performed. Events work via delegates, so let’s take a look at them first.
2. Delegates
2.1 General
A delegate is an object of type System.Delegate. It has two properties: ‘Method’ and ‘Target’. A delegate could be seen as a composition of a pointer to a method and an object. A delegate can be called just like a method. One could compare a delegate with a function pointer in C++ (although the internal implementation is very much like a functor), where you pass the object that handles the method. You can also use a delegate to refer to static methods; the ‘Target’ property then has null as value.
2.2 Usage
Let us create a delegate and use it. Make a new C# console application in Visual C# or Visual Studio. Add a class Clock.
Above the class declaration, we’ll declare a delegate, called TickDelegate. We do this by using the delegate keyword. Make sure this delegate has the public access specifier.
public delegate void TickDelegate(int hours, int minutes, int seconds);
public class Clock
{
As you can see, declaring a delegate is simple and straight forward. It is written like an ordinary method without implementation, and by making use of the delegate keyword.
In the example above, the TickDelegate delegate can refer to a method without return type, and with 3 parameters of type integer. Take into account we’ve just declared the signature of the delegate, but haven’t made an instance that refers to a method and an object.
Add a public field to the class Clock, which is able to refer to a method which matches the signature of the TickDelegate. Call this field TickHandler.
public class Clock
{
public TickDelegate TickHandler;
}
Now add a method Tick to the class Clock that checks if the TickHandler delegate isn’t null and calls it subsequentially:
public class Clock
{
public TickDelegate TickHandler;
public void Tick()
{
if (TickHandler != null)
{
var now = DateTime.Now;
TickHandler(now.Hour, now.Minute, now.Second);
Thread.Sleep(1000);
}
}
}
Now create a class ClockTester, and add a method DisplayTime that matches the signature of TickDelegate. Write the time to the console.
class Clock Tester
{
public void DisplayTime(int hours, int minutes, int seconds)
{
Console.WriteLine(“Hours {0}, Minutes {1}, Seconds {2}”,
hours, minutes, seconds);
}
}
Create an object of type ClockTester and Clock in the Main method of the Program class. Assign the DisplayTime method of the ClockTester class to the TickHandler field. Call the method Tick of the ClockTester 10 times (via a for loop).
static void Main(string[] args)
{
Clock clock = new Clock();
ClockTester tester = new ClockTester();
// the Target property of the clock.TickHandler now refers
// to tester, and the Method property refers to
// the method DisplayTime of the ClockTester class.
clock.TickHandler = new TickDelegate(tester.DisplayTime);
for (inti = 0; i < 10; i++)
{
// Here we indirectly call tester.DisplayTime
// even though the Clock class doesn’t use ClockTester.
clock.Tick();
}
Console.ReadLine();
}
The assignment statement clock.TickHandler = new TickDelegate(tester.DisplayTime) doesn’t call tester.DisplayTime; it constructs a new instance of the TickDelegate class with Target=tester and Method=DisplayTime. Since C# 2.0 the ‘new TickDelegate’ can be left from the statement, and you could just write:
clock.TickHandler = tester.DisplayTime;
3. Delegates versus interfaces
Delegates offer a form a “loose coupling”, that is very important to build components that can communicate with other classes, without knowing of their existence. Take the following example into account: a button can call methods from a class that it doesn’t know exists. What if the creator of the button class had to know what class would ever use the button…
The delegates example from above could be written using interfaces, but this would lead to bigger, less readable code. Below you can find an example of the clock example with interfaces:
interface ITickObserver
{
void HandleTick(inthours, intminutes, intseconds);
}
class Clock
{
public ITickObserver Observer;
public void Tick()
{
if (Observer != null)
{
var now = DateTime.Now;
Observer.HandleTick(now.Hour, now.Minute, now.Second);
Thread.Sleep(1000);
}
}
}
class ClockTester : ITickObserver
{
public void HandleTick(int hours, int minutes, int seconds)
{
Console.WriteLine(“Hours {0}, Minutes {1}, Seconds {2}”,
hours, minutes, seconds);
}
}
class Program
{
static void Main(string[] args)
{
Clock clock = new Clock();
ClockTester tester = new ClockTester();
clock.Observer = tester;
for (inti = 0; i < 10; i++)
{
clock.Tick();
}
Console.ReadLine();
}
}
Delegates aren’t just simpler to use – one doesn’t have to implement an interface – they also let you call methods indirectly from classed you can’t even inherit from (for example sealed classes).
4. Events and multicast-delegates
The clock class now supports only one handler (also referred to as observer or listener); in practice you almost always have multiple observers. You could simply solve this by using a List of type TickDelegate:
public class Clock
{
private List<TickDelegate> m_TickHandlers;
public void AddTickHandler(TickDelegate tickHandler)
{
if( m_TickHandlers == null )
m_TickHandlers = new List<TickDelegate>();
m_TickHandlers.Add(tickHandler);
}
public void RemoveTickHandler(TickDelegate tickHandler)
{
m_TickHandlers.Remove(tickHandler);
if (m_TickHandlers.Count == 0 )
m_TickHandlers = null;
}
public void Tick()
{
if (m_TickHandlers != null)
{
var now = DateTime.Now;
foreach (TickDelegate tickHandler in m_TickHandlers)
tickHandler(now.Hour, now.Minute, now.Second);
}
}
}
Because the code above occurs so much (this is the subject/observer pattern via delegates), there is a way to simplify this dramatically by using the so called “multicast-delegates”:
public class Clock
{
private TickDelegate m_TickHandlers;
public void AddTickHandler(TickDelegate tickHandler)
{
m_TickHandlers += tickHandler;
}
public void RemoveTickHandler(TickDelegate tickHandler)
{
m_TickHandlers -= tickHandler;
}
public void Tick()
{
if (m_TickHandlers != null)
{
var now = DateTime.Now;
m_TickHandlers(now.Hour, now.Minute, now.Second);
}
}
}
Via the += and -= operators, we can respectively add and remove objects of type TickDelegate out of the internal list of TickDelegates. We also call this “subscribe to” and “unsubscribe from” a delegate. The compiler translate these operators into the following code:
public void AddTickHandler(TickDelegate tickHandler)
{
m_TickHandlers = (TickDelegate) Delegate.Combine(m_TickHandlers,tickHandler);
}
public void RemoveTickHandler(TickDelegate tickHandler)
{
m_TickHandlers = (TickDelegate) Delegate.Remove(m_TickHandlers, tickHandler);
}
To call all TickHandlers, we don’t have to iterate the list of TickDelegates ourselves, the call
m_TickHandlers(now.Hour, now.Minute, now.Second);
does this for us. This because .Net will internally use an instance of System.MulticastDelegate as soon as more than one TickHandler is added. Very handy!
But because this case happens so much, they made a special syntax, via the event keyword:
public class Clock
{
private TickDelegate m_TickHandlers;
public event TickDelegate TickHandler
{
add { m_TickHandlers += value; }
remove { m_TickHandlers -= value; }
}
Just like properties have get / set, an event has add / remove.
To add a TickHandler, you can now write:
clock.TickHandlers += tester.DisplayTime;
and to remove a handler:
clock.TickHandlers -= tester.DisplayTime;
All the knowledge you’ve gained by reading the above yields to code below, which is much shorter and more readable than our initial case.
public class Clock
{
public event TickDelegate TickHandlers;
public void Tick()
{
if (TickHandlers != null)
{
var now = DateTime.Now;
TickHandlers(now.Hour, now.Minute, now.Second);
}
}
}
The compiler will generate the code needed for us, and we only have to type one rule of code (not taking the code to call event handlers into account).
5. Using Events
When writing a class, you should make sure it has methods, properties and events in order to make it easy to use. All Windows Forms controls use events, including the Form class.
As an example, let us rewrite the clock class, using the Windows Forms Timer. This example also shows it’s possible to use static methods as delegates; the value of the Target property is null in that case.
public delegate void TickEventHandler(int hours, int minutes, int seconds);
public class Clock
{
public event TickEventHandler Tick;
private readonly Timer m_Timer = new Timer();
public Clock()
{
m_Timer.Tick += Timer_Tick;
m_Timer.Interval = 1000;
m_Timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
if (Tick != null)
{
var now = DateTime.Now;
Tick(now.Hour, now.Minute, now.Second);
}
}
}
public class Counter
{
private int m_TickCount;
public void OnTick(int hours, int minutes, int seconds)
{
m_TickCount += 1;
Console.WriteLine(“{0}: {1}”, this, m_TickCount);
}
}
class Program
{
static void DisplayClockTime(int hours, int minutes, int seconds)
{
Console.WriteLine(“{0:D2}:{1:D2}:{2:D2}”, hours, minutes, seconds);
}
static void Main(string[] args)
{
var clock = new Clock();
var counter = new Counter();
clock.Tick += DisplayClockTime;
clock.Tick += counter.OnTick;
Application.Run();
}
}
6. Conventions
In the .NET framework, all event have the EventHandler signature (namespace System):
public delegate void EventHandler(object sender, EventArgs args);
When an event has arguments, you need to make a new class that is derived from System.EventArgs:
public class TickEventArgs : EventArgs
{
public readonly int Hours;
public readonly int Minutes;
public readonly int Seconds;
public TickEventArgs(int hours, int minutes, int seconds)…
}
The reason each eventhandler has matching signatures is that it can now be use to handle all kinds of events, even if the event uses a class that is derived from the EventArgs class.
7. Memory Leaks
Events do have some design issues you should be aware of. In .Net, the garbage collector decides when an object is to be removed. This happens when there are no more references to that object. Or more correctly, if an object can’t be reached by local, stack, global or static variables. Subscribing to an event is a strong reference. This means you also have to unsubscribe an object from the event when it’s no longer needed. If you don’t, you could create memory leaks. If an object isn’t referenced anymore, but the object is subscribed to an event, the object won’t be deleted. This because subscribing to an event adds a reference to the object via the Target property of the delegate.