A good friend of mine recently got a job as a technical reviewer - I guess he'll be reviewing code other people write for static analysis. I'm excited, because he's been a longtime Java user, and the job works with C#. So this post goes out to him, as well as all the other people looking to learn the basics of C#.
I. An event is a Signal
This might seem like a blinding flash of the obvious, but it wasn't to me. An event is a way for one object to inform one or more objects (note I did not say "other" in there - it could inform itself) of a state transition, or an external or internal stimulus.
II. Events belong to Types or Objects
Only objects or types (classes or structs, specifically) can own an event, and only the owning type (or object) can fire the event. Class-owned events must be declared with the static modifier. We'll get to the semantics of declaring and invoking an event in a short while.
III. Events are typed with a specific Delegate.
Delegates are one of the five main types of objects in the CLR (classes, structures, enums, delegates, and events). They provide a type-safe indicator of method pointers; consider the following C API declaration:
typedef VOID (WINAPI * DeferredProcedureCallProc)(LPARAM lParam);
In C, that represents a callback type called DeferredProcedureCallProc with a single parameter of type LPARAM, and has no return value. However, C does not have a mechanism for enforcing type safety with this type of procedure; C#, however, does: it has delegates. Delegates are declared with the following syntax:
[access-modifier] delegate return-type identifier([parameter-list]);
Accounting for the C# type conversion of LPARAM to IntPtr, the above C callback can thusly be translated to:
1: public delegate void DeferredProcedureCallback(IntPtr lParam);
That means that an object of type DeferredProcedureCallback can be instantiated, referencing any method that has no return value and accepts an IntPtr as a parameter.
When a delegate is instantiated, it encapsulates two specific pieces of information: the method it reference, and the object it references. Consider this example:
1: public class Foo
3: public Foo()
5: DeferredProcedureCallback dpc = new DeferredProcedureCallback(this.Bar);
7: public void Bar(IntPtr info)
Note that the Bar function is referenced without parameters. The dpc variable also holds a reference to the Foo object it is accessing - so that only that specific instance of Foo will be notified.
Returning to the concept of events - events are semantically declared within a class in the following way:
[access-modifier] [static | ( abstract | virtual | override | sealed )] event delegate-type identifier;
An example of this would be:
1: public class Button
3: public event EventHandler Click;
In this, example, an event is declared that utilized the System.EventHandler delegate.
IV. Events can signal multiple targets, and are called as methods
One of the neat things about events is that, at compile-time, delegates are all derived from System.MulticastDelegate. A multicast signal is one which targets specific objects, based on the fact that these objects registered to be signaled (as opposed to a broadcast signal, which doesn't require pre-registration). Any number of delegates can be registered with a given event, or unregistered. To register, we utilize the += operator (and to unregister, we use the -= operator) to indicate the inclusion or removal of a given event handler.
Here is a sample registration of an event handler using a Button control on a Windows Form:
1: public class MyForm : System.Windows.Forms.Form
3: private Button myButton;
5: public MyForm()
7: myButton = new Button();
9: myButton.Click += new EventHandler(this.myButton_Click);
12: private void myButton_Click(object sender, EventArgs e)
14: MessageBox.Show("I was clicked!");
In this example, a new Button was created on the form, and its Click event is handled by the myButton_Click method of this object.
I will get into why the following pattern is used in the next section, but here's the code to use for signaling the Click event:
1: public class Button : Control
3: public event EventHandler Click;
4: protected virtual void OnClick(EventArgs e)
6: if (Click != null)
7: Click(this, e);
All you would need to do is call OnClick directly, and the Click event will be signaled.
Note that you should check the event as to whether it's null (for nullillity?) because otherwise a NullReferenceException will be thrown by the runtime. Finally, note that the Click event is called with the same signature as the delegate of the event.
V. Best Practices, Guidelines, and Advanced Information
The .NET Framework Best Practices and Guidelines indicate that you should utilize certain patterns. The above sample illustrates the pattern of implementation that should be utilized: an event, and a calling virtual method with "On" prepended to the identifier. The idea is that derived classes can override the default implementation and invoke the parent class's implementation to invoke the event. For example, consider an ImageButton class:
1: public class ImageButton : Button
3: protected override void OnClick(EventArgs e)
7: this.ImageUrl = GetPressedStateImageUrl();
So, rather than needing to handle an event, the code is able to capture the Click event without specifically using the Click event.
Other best practices can be found in Event Usage Guidelines in the MSDN Library.
Finally, as an aside, just like properties, events can define event accessor blocks. By default, the compiler will create these blocks; it might be worthwhile to look into these for certain specific implementations. For more information, refer to the C# Language Specification, 10.7.2 - Event accessors.
I hope this overview was worthwhile! Events can be exceptionally useful and a new way of looking at cross-object communication; it is no longer necessary to create an entire interface and class to handle an event. The compiler can enforce type safety, and signaling multiple objects is incredibly easy. Use them!