Download source files - 1.15 Kb
Introduction
All of us have been exposed to event driven programming of some sort or the
other. C# adds on value to the often mentioned world of event driven programming
by adding support through events and delegates. This article is a part of a
series that aims at understanding fully the way in which delegates and events
operate. Part1 helps you understand the role of multicast delegates in the
context of UI interaction. The emphasis of this article would be to identify
what exactly happens when you add an event handler to your common UI controls. A
simple simulation of what could possibly be going on behind the scenes when the
AddOnClick or any similar event is added to the Button class will be explained.
This will help you understand better the nature of event handling using multi
cast delegates. All the examples provided are in C#.
Events
An event is the outcome of an action. There are two important terms with
respect to events. The event source and the event receiver. The object that
raises the event is called event source and the object that responds to the
event is called event receiver. Did you observe a hitch in the above
explanation? O.K. my event source raises the event and my event receiver
receives it, but what is the communication channel between them. Take the
instance of you talking with a friend of yours over the telephone. Now you are
the event source and your friend is the event receiver and the telephone cable
is the communication channel. Similarly the communication channel between an
event source and an event receiver is the delegate. The internals of events and
their relationship to delegates would be dealt with in a later article. The
explanation of events here is just to help the reader understand and visualize
what exactly events are.
Delegates
Forget computers for a moment. Just think about delegates in the real world.
Who are delegates? Delegates are people who represent a particular country or
region or state in another country or region or state. Extend this same
definition now to C#. In C#, delegates act as an intermediary between an event
source and an event destination.
To be even precise delegates are similar to function pointers. They can be
called as type safe function pointers. Delegates have other uses in addition to
event handling. But this article would focus on describing delegates with
respect to event handling. A delegate class used as an intermediary between an
event source and event receiver is called an event handler.
Types of Delegates
There are basically two types of delegates. Single Cast delegate and Multi
Cast delegate. Let us first understand at the broader level the definition of
both these delegates. A single cast delegate can call only one function. A multi
cast delegate is one that can be part of a linked list. The multi cast delegate
points to the head of such a linked list. This means that when the multi cast
delegate is invoked it can call all the functions that form a part of the linked
list. Assume that i have several clients who would like to receive notification
when a particular event occurs. Putting all of them in a multi cast delegate can
help call all the clients when a particular event occurs.
To support a single cast delegate the base class library includes a special
class type called System.Delegate. To support multi cast delegates the base
class library includes a special class type called SystemMultiCastDelegate.
Single Cast delegate
The signature of a single cast delegate is shown below. The letters in
italics can be replaced with your own names and parameters.
public delegate Boolean DelegateName (parm1, parm2)
When the compiler compiles the statement above, it internally generates a new
class type. This class is called DelegateName and derives from System.Delegate.
Just to make sure just check the ILDisassembler code to check that this is
happening.
As an example let us create a single cast delegate named MyDelegate which
would point to a function MyFunction. The code appears as below,
public delegate Boolean MyDelegate(Object sendingobj, Int32 x);
public class TestDelegateClass
{
Boolean MyFunction(Object sendingobj, Int32 x)
{
return (true);
}
public static void main(String [] args)
{
MyDelegate mdg = new MyDelegate(MyFunction);
TestDelegateClass tdc = new TestDelegateClass();
Boolean f = mdg(this, 1);
}
}
How does the above code work?
The System.Delegate contains a few fields. The most important of them are
Target and Method.
The Target field identifies an object context. In the scenario above this
would point to the TestDelegateClass being created. If the method to be invoked
is a static method then this field will be null.
The Method field identifies the method to be called. This field always has
some value. It is never null.
To the intelligent reader, if you observe the IL Disassembler code you would
see that the constructor for MyDelegate contains two parameters. But in our case
we are passing only one parameter? Any guesses on how this is done? Yeah you are
right! It gets done internally by the compiler. The compiler resolves the call
above to a call passing in the two parameters required. If you observe in C++
with managed extension, the object and the address of the function have to be
passed explicitly. But in VB and C# we have done away with this with the
compiler filling in the necessary details.
In the code sample above we saw,
Boolean f = mdg(this, 1);
When the compiler comes across this line, this is what happens,
- The compiler identifies that mdg is a delegate object.
- The delegate object has the target and method fields as described above.
- The compiler generates code that calls the Invoke method of the
System.Delegate derived class i.e MyDelegate.
- The Invoke method internally uses the MethodInfo type identified by the
delegate's Method field and calls the MethoidInfo's invoke method.
- The MethodInfo's invoke method is passed the delegates Target field which
identifies the method and an array of variants which contains the parameters to
the method to be called. In our case, the array of variants would be an Object
and an Int32 value.
The above discussion would have made you clear about what happens internally
when a method gets called through a Single Cast delegate.
Multi Cast delegate
The signature of a mutli cast delegate is shown below. The letters in italics
can be replaced with your own names and parameters.
public delegate void DelegateName (parm1, parm2)
Whatever has been explained with respect to Single cast delegate holds good
even in the context of a Multi Cast Delegate. There is a small addition here.
Since a multi cast delegate represents a linked list, there is an additional
field called prev which refers to another Multi Cast Delegate. This is how the
linked list is maintained.
The return type if you have observed has changed from Boolean to void. Have
you guessed the reason as to why it happens? The reason is that since several
multi cast delegates get called consecutively we cannot wait to get the return
value from each of these methods being called.
The code shown in the example for single cast delegate will work in the case
of multi cast delegate too. The only difference is that the delegate signature
needs to be changed, the method signature needs to be changed to return void
instead of Boolean.
The power of multi cast delegates come in when you combine delegates to form
a linked list. The Combine method is available in the System.Delegate class as a
static method. The function signature as follows.
public static Delegate Combine(Delegate a, Delegate b);
What the above method does is that it combines the two delegates a and b and
makes b's prev field point to a. It returns the head of the linked list. This in
turn has to be type casted to our delegate type.
Similar to the Combine method we have a remove method which removes a
delegate from the linked list and gives you the modifies smaller linked list
with a pointer to the head. The signature of the remove method is as follows,
public static Delegate Remove(Delegate source, Delegate value);
Here's a small sample that illustrates the use of a multi cast
delegate,
using System;
class MCD1
{
public void dispMCD1(string s)
{
Console.WriteLine("MCD1");
}
}
class MCD2
{
public void dispMCD2(string s)
{
Console.WriteLine("MCD2");
}
}
public delegate void OnMsgArrived(string s);
class TestMultiCastUsingDelegates
{
public static void Main(string [] args)
{
MCD1 mcd1=new MCD1();
MCD2 mcd2=new MCD2();
OnMsgArrived oma=new OnMsgArrived(mcd1.dispMCD1);
OnMsgArrived omb=new OnMsgArrived(mcd2.dispMCD2);
OnMsgArrived omc;
omc=(OnMsgArrived)Delegate.Combine(oma,omb);
Delegate [] omd;
omd=omc.GetInvocationList();
OnMsgArrived ome;
for(int i=0;i<omd.Length;i++)
{
ome=(OnMsgArrived)omd[i];
ome("string");
}
}
}
The examples in this article would revolve around Multi Cast Delegates. It is
essential that you have understood the basics of single cast and multi cast
delegates before you proceed further.
Problem Scenario
The code below shows an example of adding a button to a Form. The button is
associated with an event handler that handles whatever happens when the button
is pressed. When the form is displayed and the button is pressed a message box
showing "Button Clicked" will appear.
Compilation
directions: Compile using
csc /r:System.DLL;System.WinForms.DLL;Microsoft.Win32.Interop.DLL FormTest.cs
using System;
using System.WinForms;
public class FormTest : Form
{
private Button button1 = new Button();
public static void Main (string [] args)
{
Application.Run(new FormTest());
}
public FormTest()
{
this.Text = "Hello WinForms World";
button1.Text = "Click Me!";
button1.AddOnClick(new System.EventHandler(buttonClicked));
this.Controls.Add(button1);
}
private void buttonClicked(object sender, EventArgs evArgs)
{
MessageBox.Show("Button clicked");
}
}
The topic of our discussion would be the routing of calls between the time
when the click event is raised and the time when the method displaying
"Button clicked" is displayed.
Components of the Exercise
This section will talk about the components that would complete the
architecture,
1. Delegate Event Handler
A delegate event handler that routes the call to the appropriate method. Let
us call it ButtonEventHandler. The declaration of this delegate should follow
that the same for declaration of multicast delegates. The declaration is as
follows.
public delegate void ButtonEventHandler(object sender, ButtonEventArgs e);
Parameters
sender - Identifies the origin of the event
ButtonEventArgs - Identifies the Event Arguments. This class is derived from
EventArgs. EventArgs is the base class for event data. The data with respect to
the event generated is going to be held in this class. The reason why I have
made use of a seperate class is that i am going to use a string member variable
in the derived class which identifies the the action.
2. EventArgs derived class
The reason for using a derived class for EventArgs has been mentioned in the
previous point. The class looks like this:
public class ButtonEventArgs : EventArgs
{
public string msg;
public ButtonEventArgs(string message)
{
msg=message;
}
}
3. A button class
This class is the simulation of the NGWS SDK Button Class. A control class
such as a button class must be able to generate events when actions such as
clicking on it are performed. Now think logically, the button is pressed and a
message indicating the same should be displayed. The button here is the event
source. Forget for a moment about the event receiver. Now what is required in
between? Yeah! you guessed it right, a delegate to forward the calls to the
appropriate event receiver. Now if, you want to add a delegate to your button
you do it through Add methods in your button class. Similarly if you want to
remove a delegate you do it through a Remove method in your button class. These
methods must be named AddOn<Event Name> and RemoveOn<Event Name>
according to the NGWSSDK documentation. Our button class will respond to click
and press events. Thus the methods in our class should be AddOnPress,
RemoveOnPress, AddOnClick and RemoveOnClick. The signature of the above methods
will be as follows,
public void AddOnClick(ButtonEventHandler handler);
public void RemoveOnClick(ButtonEventHandler handler);
public void AddOnPress(ButtonEventHandler handler);
public void RemoveOnPress(ButtonEventHandler handler);
At the end of this we have added the methods which add and remove delegates
corresponding to Click and Press events in the button class.
Now what is required? The most important part, methods that raises the
events. The naming conventions followed for such methods are On<Event
Name>. Thus the methods in our class should be OnClick and OnPress. These
methods are passed the event data. The method raises the events by invoking the
delegates.
The signature of the mehods will be as follows,
protected virtual void OnPress(ButtonEventArgs e);
protected virtual void OnClick(ButtonEventArgs e);
Why are the above two methods declared as protected? To help the subclasses
override this event without attaching a delegate to it.
What happens in the SDK Button class. When the button is pressed, an event
data gets created, the OnClick method gets called passing in the event data as
the parameter. The OnClick method in turn calls the event receivers through the
delegates. Do you observe a missing link above? If the OnClick method gets
passed the event data, who creates the event data? The answer is that the event
data could be created by the SDK. For our purposes since we cannot tap onto the
SDK let us simulate what the SDK does by creating two methods whose only purpose
is to create the event data and call the appropriate OnClick or OnPress methods.
The signature of these methods is as follows,
public void click();
public void press();
Now
let us take a look at the whole button class,
class button
{
private ButtonEventHandler beh=null;
private ButtonEventHandler bep=null;
public void AddOnClick(ButtonEventHandler handler)
{
Console.WriteLine("Adding click event handler");
beh=(ButtonEventHandler)Delegate.Combine(beh, handler);
}
public void RemoveOnClick(ButtonEventHandler handler)
{
Console.WriteLine("Removing click event handler");
beh=(ButtonEventHandler)Delegate.Remove(beh, handler);
}
protected virtual void OnClick(ButtonEventArgs e)
{
if (beh!=null)
{
beh(this, e);
}
}
public void AddOnPress(ButtonEventHandler handler)
{
Console.WriteLine("Adding Press Event handler");
bep=(ButtonEventHandler)Delegate.Combine(bep, handler);
}
public void RemoveOnPress(ButtonEventHandler handler)
{
Console.WriteLine("Removing Press Event handler");
bep=(ButtonEventHandler)Delegate.Remove(bep, handler);
}
protected virtual void OnPress(ButtonEventArgs e)
{
if (bep!=null)
{
bep(this, e);
}
}
public void click()
{
ButtonEventArgs bea=new ButtonEventArgs("clicked");
OnClick(bea);
}
public void press()
{
ButtonEventArgs bea=new ButtonEventArgs("pressed");
OnPress(bea);
}
}
Now
let us begin a line by line walkthrough of the above code,
private ButtonEventHandler beh=null;
private ButtonEventHandler bep=null;
The
above two lines declare two delegates. Both these delegates are multicast
delegates. They represent the series of method calls that have to take place
when an event occurs. They are initialized to null to indicate that they point
nowhere when the class is first instantiated. I am using two delegates here
because I assume that separate delegates are used to track separate events. In
the code snippet above, the delegate beh would handle click calls and the
delegate bep would handle press calls.
public void AddOnClick(ButtonEventHandler handler)
{
Console.WriteLine("Adding click event handler");
beh=(ButtonEventHandler)Delegate.Combine(beh, handler);
}
The
AddOn methods are used to add a list of methods that need to get called when an
event occurs. Thus it is natural that it receives a delegate as a parameter
which in turn points to a function. Depending on the number of methods that
needs to be invoked when an event occurs, this method is invoked multiple times
passing the appropriate delegate parameter. The situation can be better
explained with the help of a figure,
beh ------> ButtonEventHandler1 -----> &Fn1()
ButtonEventHandler2 -----> &Fn2()
ButtonEventHandler3 -----> &Fn3()
ButtonEventHandler4 -----> &Fn4()
As the figure above shows, the beh pointer points to a delegate which in turn
points to a function. In the case of multicast delegate, there is a prev field
which gets initialized to null when the delegate is constructed. Now when you
invoke the combine method as shown in the code snippet above, the prev field of
handler points to beh. Thus when many delegates are combined using a Combine
method, what internally happens is that a linked list gets created. That is why
when we invoke the delegate from the OnClick or OnPress methods we find that all
the functions combined get called. This makes it possible for multiple clients
to receive notification about an event. This is the funda behind multi cast
delegates.
The same explanation applies to RemoveOn methods.
4. The Form Simulation
In our form example explained in the beginning of the article we had a form
class derived from Form which constructed a button and attached event handlers.
We are going to simulate the same but the only difference would be that we would
not be creating a GUI but would instead have a client which adds our simulated
button class and attaches event handling methods to it. For a change, instead of
explaining the source code in bits and pieces let me put in explanations in the
source code itself.
The code for the client class is as follows,
public class DelegatesAndEvents
{
private button button1=new button();
private DelegatesAndEvents()
{
button1.AddOnClick(new ButtonEventHandler(ClickEventHandler1));
button1.AddOnClick(new ButtonEventHandler(ClickEventHandler2));
button1.AddOnClick(new ButtonEventHandler(ClickEventHandler3));
button1.AddOnPress(new ButtonEventHandler(PressEventHandler));
button1.click();
button1.press();
}
public static void Main(String [] args)
{
DelegatesAndEvents d=new DelegatesAndEvents();
}
public void ClickEventHandler1(object sender, ButtonEventArgs e)
{
Console.WriteLine("click event handler1");
Console.WriteLine(e.msg);
Console.WriteLine("after");
}
public void ClickEventHandler2(object sender, ButtonEventArgs e)
{
Console.WriteLine("click event handler2");
Console.WriteLine(e.msg);
Console.WriteLine("after");
}
public void ClickEventHandler3(object sender, ButtonEventArgs e)
{
Console.WriteLine("click event handler3");
Console.WriteLine(e.msg);
Console.WriteLine("after");
}
public void PressEventHandler(object sender, ButtonEventArgs e)
{
Console.WriteLine("press event handler");
Console.WriteLine(e.msg);
Console.WriteLine("after");
}
}
Output
Conclusion
What goes on behind the scenes when event handlers are added? How do
delegates aid in this? How can the event handling in a class be simulated, how
do do multiple methods get called when a particular event has been raised etc.
have been the core points of discussion dealt with in this article. I hope that
the explanations would have helped in bringing about some amount of clarity.
Some aspects are only internally known to the runtime and no effort has been
made to dwelve into them. One such example is how does the click event get
raised when the button is actually clicked. The article has focused only on what
is absolutely necessary for us to understand the way delegates and events
interrelate in the context of an event handler.