Introduction
We are going to discuss about the most familiar topic Events. Common Language Runtime (CLR) events are based on delegate type class members. Delegates are type safe to invoke callback methods. In callback methods, the object will receive the notification that it subscribed to. I am not going to talk about delegates. Let us move to events. We all know the button class that has an event called Click
. When the button is clicked, the object registered with the events wants to receive the notification so that it will perform some action.
In this article, I have created the custom event program called TimeAlert
. TimeAlert
program gets the system time and checks whether it is scheduled. If it is scheduled, then it will display the message. And I will explain the complete class of event registration and unregistration using add and remove method and thread synchronization using add and remove accessors.
When the event is raised, the object raising the event wants to pass some additional information to the object receiving the event notification. Here we have created the custom EventArgs
named TimeEventArgs
. The information is encapsulated using the private
and public
readonly field. This has been created by inheriting the base class System.EventArgs
. We have to suffix the word eventargs
in the derived class name. In the below example, the public
readonly variable (strMessage
) holds the information.
Using the Code
public class TimeEventArgs : EventArgs
{
public string Message;
public TimeEventArgs(string strMessage)
{
Message = strMessage;
}
}
The implementation of System.EventArgs
class in the .NET Framework Class Library is as shown below:
[ComVisible(true)]
[Serializable]
public class EventArgs
{
public static EventArgs Empty = new EventArgs();
public EventArgs()
{
}
}
If there is no need to pass any additional information to the receiver, then you can simply use the System.EventArgs
rather than creating a new EventArgs
object. Many events don't have additional information to pass on. For example, invoking the callback method is enough information for the button
class. In such a case, there is no need to create a custom EventArgs
object. We can simply say like the System.EventArgs
serves as a base type for the other types to derive.
Declaring a Delegate to Handle Custom Event
We can simply say that delegate acts like an eventhandler
. So that we can raise the events from anywhere in the application. In the below example, the delegate has two parameters, the first one is sender which holds object type and the second one is an custom eventargs
. Many of the developers might have a doubt why this sender parameter should always be an object type. It is simple that the "sender
" argument is the object that raised the event. We all know about the second parameter which holds some additional information. We have created the custom eventargs
and a delegate. It is time to create the event and to raise it from the class so that the user can subscribe to it. When we raise the event, it will provide the sender information and the instance information.
public class TimeAlarm
{
public delegate void AlarmEventHandler(object Sender, TimeEventArgs e);
public event AlarmEventHandler TimeEvent;
}
ILdasm.exe Displaying the Metadata Produced by the Compiler for the Event
The compiler will compile the below single line of code into three constructs:
public event AlarmEventHandler TimeEvent;
- Private delegate field initialized to
null
:
private AlarmEventHandler TimeEvent = null;
- A
public add_ TimeEvent
method. This allows an object to register interest with an event:
[MethodImpl(methodImplOptions.Synchronized)]
public void add_TimeEvent( AlarmEventHandler value)
{
TimeEvent = (AlarmEventHandler) Delegate.Combine(TimeEvent,value);
}
- A
public add_ TimeEvent
method. This allows an object to unregister interest with an event:
[MethodImpl(methodImplOptions.Synchronized)]
public void remove_TimeEvent( AlarmEventHandler value)
{
TimeEvent = (AlarmEventHandler) Delegate.Remove(TimeEvent,value);
}
The compiler has declared that the private
field in the first construct is a delegate type. This field refers to a list of delegates. When an event occurs, this field will be notified. Initially, this field is set to null
meaning that no listeners have been registered in this event. When a method registers interest with an event, then this field refers to an instance. Whenever a listener registers interest in an event, the listener is simply adding an instance of the delegate type to the list. The compiler has generated this type as private
because of its protection meaning that it may be mishandled outside of the class.
The second and third construct of C# compiler auto generated method is to register and unregister the object interest with an event. It uses the prefix “add_
” and “remove_
” for naming the methods. The register method uses the Delegate
class and a static
method Combine
to add the instance of a delegate to the list of delegates and returns the list. To unregister an object, the method uses the Delegate
class and static
method Remove
(instead of Combine
) to remove the instance of a delegate to the list of delegates and returns the list. This is what happens internally when we are using the “+=
” and “-=
” to register/unregister with an event.
You can see the attribute [MethodImpl(methodImplOptions.Synchronized)]
added in the second and third construct. Multiple listeners that can register/unregister with the event may corrupt the delegate list. But the above given attribute will synchronize that only one add or remove method can be used at any one time which is defined in the System.Runtime.CompilerServices
namespace. Thread synchronization is required so that the list of delegate objects doesn't become corrupted.
We have used the MethodImpl
attribute in an instance method. Now the CLR uses the object itself, the thread synchronization lock. Think if the class uses many events all the add and remove methods use the same lock. It will affect the scalability. If multiple threads use the same lock for registering and unregistering with the events, it will affect the performance. But it is a very rare scenario. The Thread
synchronization guideline states that it should not take the lock on objects, because the object is exposed publicly. It will cause deadlock if we are running in a multi threading environment.
private object eventlock = "";
public delegate void AlarmEventHandler(object Sender, TimeEventArgs e);
private AlarmEventHandler m_TimeEvent;
public event AlarmEventHandler TimeEvent
{
add
{
lock (eventlock)
{
m_TimeEvent += value;
}
}
remove
{
lock (eventlock)
{
m_TimeEvent -= value;
}
}
}
We have created the add and remove context explicitly instead of giving the burden to the compiler. Now see the first line. We have declared a private
object which is used for thread synchronization lock. A private
field named m_TimeEvent
which holds the listener. We have extended the syntax after the event initialization. Within that we have added two accessors named add and remove which are mainly used to register and unregister the events. The main thing that we have ignored is the line [MethodImpl(methodImplOptions.Synchronized)]
because our code needs proper thread synchronization lock.
namespace CustomEvents
{
class Program
{
static void Main(string[] args)
{
TimeAlarm objTimeAlarm = new TimeAlarm();
objTimeAlarm.TimeEvent += new TimeAlarm.AlarmEventHandler(CallAlarmMethod);
Console.WriteLine("System time: " + System.DateTime.Now.ToString("HH.mm"));
double dltime = Double.Parse(System.DateTime.Now.ToString("HH.mm"));
objTimeAlarm.ActivateTimeAlarm(dltime);
Console.ReadLine();
}
public static void CallAlarmMethod(object Sender, TimeEventArgs e)
{
Console.WriteLine(e.Message);
}
}
public class TimeEventArgs : EventArgs
{
public string Message;
public TimeEventArgs(string strMessage)
{
Message = strMessage;
}
}
public class TimeAlarm
{
private object eventlock = "";
public delegate void AlarmEventHandler(object Sender, TimeEventArgs e);
private AlarmEventHandler m_TimeEvent;
public event AlarmEventHandler TimeEvent
{
add
{
lock (eventlock)
{
m_TimeEvent += value;
}
}
remove
{
lock (eventlock)
{
m_TimeEvent -= value;
}
}
}
public void ActivateTimeAlarm(double dlTime)
{
string strMessage = string.Empty;
if ((dlTime >= 10.30 && dlTime <=10.45))
strMessage = "Break";
else if (dlTime >= 13.00 && dlTime <= 14.00)
strMessage = "Lunch";
else if (dlTime >= 18.00)
strMessage = "Fill your timesheet";
else if (dlTime >= 9.30)
strMessage = "Working";
TimeEventArgs objEvent = new TimeEventArgs(strMessage);
AlarmEventHandler temp = m_TimeEvent;
temp(this, objEvent);
}
}
}
Output
History
- 27th April, 2009: Initial post