It's time to discuss events in C#, since we had a good simple discussion about C# delegates in the past article. Today we explain what events are, how they are implemented, why they are exist and when to use them.
Contributed by Michael Youssef Rating: / 53 February 13, 2007
I will show you how to create something that is event-like without an event and why it's a bad practice; I show you this technique to help you understand the design of .NET events. Also we will look at the conventions of naming our classes and methods that form the event model. If you are not familiar with delegates I suggest reading my article "C# Delegates Explained" because the concepts and techniques discussed here are based on that article.
As you may remember from the discussion in the previous article, C# Delegates Explained, a delegate is an object that refers to a method, and we call this method by invoking the delegate itself. If you think about this for a minute you will realize that the event model is based on delegates. You must be familiar with events. An event is simply a notification of something that took place, such as a Windows form being loaded, a mouse button being clicked, a book being added to the book shop and other numerous behaviors.
We need a way to take some actions when an event is fired (or we can say took place) and this is introduced in C# through the use of delegates and event handler methods as we are going to see shortly. The event model is designed so that an object fires an event and another object (or objects) watches the occurrence of this event and calls a method to perform the action needed. The object that fires the event is called the publisher of the event and the object that has the method, which is called an event handler method, that is called when the event takes place is called the watcher object.
How do delegates fit into the game? The publisher object doesn't know which event handler method of which watcher object needs to be called when the event takes place. Through the use of delegates any object (such as the watcher object) that needs to be notified about the occurrence of the event will have to create a new instance of a delegate (the type of the delegate is discussed later) that refers to the event handler method, then assigns (thus subscribing) this delegate to the publisher object's event. The event maintains a multicast delegate, and when the event takes place the multicast delegate invokes its invocation list of delegates, causing all the subscriber objects' (the watcher objects') event handler methods to be called.
I know that you may be confused about how it's done, but by the end of this article you will understand the usefulness of delegates and events together. Let's begin with a very simple example, a Windows Form example.
Compile the code using the C# compiler with the command: csc /t:winexe windowsapp.cs
Double click on the file windowsapp.exe then click on the button of the form, and you will get a message box as shown in the next figure:
The SimpleForm class inherits the System.Windows.Forms.Form class which represents a window. The Main method of this class begins running a message loop then makes the form visible, using the method Application.Run(). Actually we are not interested in the code of the Main method because it's beyond the scope of this article. The Constructor of the SimpleForm class creates an instance of the class System.Windows.Forms.Button and sets some properties (Name and Text), then Subscribes to the Click event of this button using the += operator. As we have said before, the implementers of the button class don't know which method of which object needs to be notified of the occurrence of the click event.
Through the use of delegates they guarantee that a watcher object (like a SimpleForm instance) defines a method, an event handler method, then creates an instance of a specific delegate type that encapsulates this method and assigns (subscribes) the delegate instance to the Multicast delegate maintained by the click event. As you can see in the code, we assign a new instance of the delegate type System.EventHandler to the click event of the button1 object. The EventHandler delegate encapsulates methods that accepts two parameters, the first of type object and the second of type EventArgs, and return void.
public delegate void EventHandler(Object sender, EventArgs e);
The EventArgs class is the base class for the classes that carry event data. You may not understand how useful this mechanism is until you complete the article.
The method button1_Click matches the signature of the delegate so we use it to subscribe to the Click event through the EventHandler delegate instance. When you click on the button the click event takes place, which invokes its multicast delegate which in turn calls the subscribers' event handler methods like our button1_Click, which shows a message box. Let's discuss how we can create our own event.
The very first step in creating an event is to define the signature of the event handler methods of the watcher objects in a delegate type. Why? Because events are based on delegates which encapsulate methods with a specific signature. As you know .NET Delegates are type-safe. Then you create a class that raises the event, the Publisher. In this class you define the event of the specific delegate type using the event keyword then define a method that fires the event. The last step is to create a watcher object that has an event handler method that will be called when the event takes place.
Let's assume that we have a bookshop which fires an event each time a book is added to the bookshop. Another object needs to be notified each time a book is added to the bookshop to send the members an e-mail message to tell them about the new book. We need the following classes in order to create an event-based application:
The Book class which represents a book.
The BookShop class that fires the event.
The AddBookEventHandler Delegate type that defines the signature of the event handler methods.
The BookEventArgs class to carry the event data.
The Notifier class which represents the watcher object.
The Sys class that contains the Main() method which creates the objects and test the event.
Let's create those classes step by step.
The Book Class
The Book class is very simple. It has three private fields with their public properties and one constructor.
public class Book { private string title; private string isbn; private decimal price;
public delegate void AddBookEventHandler(object source, BookEventArgs e);
The delegate encapsulates any method that takes two parameters, an object and a BookEventArgs instance, and returns void. Note that the BookEventArgs class, which is implemented in the next section, derives the EventArgs class. When you need to carry data for the event you must create a class that derives the EventArgs class. Next, let's look at the implementation of the BookShop class.
public class BookShop { private ArrayList books; private string name; public event AddBookEventHandler AddBook;
public BookShop(string name) { books = new ArrayList(); this.name = name; }
public void AddNewBook(Book book) { this.books.Add(book); BookEventArgs e = new BookEventArgs(book); OnAddBook(e); } }
The Bookshop class contains a private ArrayList to store the books, but the interesting code begins with the declaration of the event. We have declared an event called AddBook of type AddBookEventHandler delegate with the event keyword. Why do we need to do this?
When you define an event as of a specific delegate type you standardize the signature of the methods that can be notified (through delegates) of the occurrence of that particular event.
Also the event creates a multicast delegate (of the delegate type it has been declared with). To maintain the assigned delegates of the watcher objects, you assign a delegate instance to a multicast delegate through the += operator.
The method OnAddBook() raises the AddBook event. It simply checks if the multicast delegate of the event is not null then calls the delegate using the event identifier. The method AddNewBook(), which calls the OnAddBook() method, accepts a Book instance as a parameter, then it adds the instance to the private ArrayList object. The method creates a new instance of the class BookEventArgs and passes the Book instance to the constructor. Then it calls the OnAddBook() method and passes the BookEventArgs instance to it.
As we said, the OnAddBook() method raises the event by invoking the multicast delegate. The signature of the delegate takes two parameters: the first is of type object which represents the object that raised the event and that's why we passed the this keyword and the second is the BookEventArgs instance, which is created by AddNewBook() then passed to OnAddBook() method, which passes it to the multicast delegate:
AddBook(this, e);
Client code should call the AddNewBook() method which in turn calls the method OnAddBook() that raises the event.
The BookEventArgs Class
The BookEventArgs class derives the EventArgs class. It contains data (the Book object newly added to the bookshop) for the event. Imagine that you subscribed to the AddBook event but you don't have access to the newly created Book instance inside the event handler method. I don't want to imagine something like that anyway. Here's the code of the class:
public class BookEventArgs: EventArgs { private Book book; public BookEventArgs(Book book) { this.book = book; }
public Book Book { get { return this.book;} } }
This class is passed the Book instance when it's created in the BookShop.AddNewBook() method so there's nothing special here. It takes the Book instance and stores it in a private field. You can read the object through the public property.
The Notifier class has only one method that can be hooked up to the event through the delegate that will be created in the Sys Class's Main() method. The Method is called instance_AddBook(), but you can call it any name you like, and it has the same signature as the delegate in our example. Note that the method has access to the newly added book through the BookEventArgs parameter; thus we have created an event with data that is passed to the event handler method (the instance_AddBook() method). The method prints a message to the console to notify us that a message has been sent to the members about the book. Here's the code of the Notifier class:
public class Notifier { public void instance_AddBook(object source, BookEventArgs e) { Console.WriteLine("A Message has been sent to the Membersn" + " regarding the book '{0}' with the ISBN '{1}'n", e.Book.Title, e.Book.ISBN); } }
The Sys Class is the application that uses all the above classes. Let's look at the code:
public class Sys { public static void Main() { BookShop bookShop = new BookShop(".NET Bookshop");
Book b1 = new Book("The Right Way","123456789", 19.90m); Book b2 = new Book("The .NET Stuff","987654321", 29.90m);
Notifier notify = new Notifier();
bookShop.AddBook += new AddBookEventHandler(notify.instance_AddBook);
The Main() method creates an instance of BookShop, then creates two Books, a Notifier instance then hooks up the event handler method, notify.instance_AddBook() through a delegate type AddBookEventHandler instance, to the event AddBook through the += operator. Note that you can't use the assignment operator = because the event maintains a multicast delegate and if C# lets you assign a delegate using the = operator you would replace the multicast delegate with the new value which is just a delegate (singlecast delegate).
The bookShop.AddNewBook() takes a Book instance to add it and then it fires the event as we discussed. You need to put all the above classes into a namespace -- I called it Events -- then reference the System.Collections namespace because we have used an ArrayList object to store Book instances. Copy the classes to the VS.NET Class file and run it.
As you can see, the event handler method is called twice because the event has been fired twice. Also note that the use of the class BookEventArgs gives us the ability to tell the members about the Title and ISBN of the book through the properties of the Book instance of the BookEventArgs. Using the EventArgs class would give us the ability to tell them that a book has been added, but we can't tell them any information about the book because we don't have access to that object. I hope that by now you understand the event model and how it's useful for our applications.
By convention, the delegate identifier ends with EventHandler and the event identifier is up to you, but it has to describe an action that can happen, like a mouse click or a form load and so on. The name of the class that contains the event data ends with EventArgs and it must derive the System.EventArgs base class. The name of the method that raises the event must begin with On like our OnAddBook() method and like many other methods you are going to work with in Windows.Forms.
MSIL Generated Code
Load the application with ILDASM, navigate to the BookShop class and expand it.
Even though we have created the event AddBook as public it's private in the MSIL code as highlighted in the above screen shot. Note that the private field is of type Events.AddBookEventHandler which means that this field is using a delegate of type AddBookEventHandler to store the delegates that are called when the event takes place. The method Add_AddBook() is generated to add a delegate instance to the Multicast AddBookEventHandler delegate of the event, of course using the Delegate.Combine() method. The method remove_AddBook() is generated to remove a delegate from the event through the Delegate.Remove() method. The event itself is the member AddBook; here's the MSIL generated code:
.event Events.AddBookEventHandler AddBook { .addon instance void Events.BookShop::add_AddBook(class Events.AddBookEventHandler) .removeon instance void Events.BookShop::remove_AddBook(class Events.AddBookEventHandler) } // end of event BookShop::AddBook
As you can see, it's very similar to a property (get and set methods) but here, you see add and remove behavior. So you may say that we can do the same event model with delegates only without the use of events. Let's do exactly that.
public string Title { get{return this.title;} } public string ISBN { get{return this.isbn;} } public decimal Price { get{return this.price;} } }
public class BookEventArgs: EventArgs { private Book book; public BookEventArgs(Book book) { this.book = book; }
public Book Book { get { return this.book;} } } public class Notifier { public void instance_AddBook(object source, BookEventArgs e) { Console.WriteLine("A Message has been sent to the Membersn" + " about the book '{0}' with the ISBN '{1}'n", e.Book.Title, e.Book.ISBN); //((BookShop)source).AddBook = null; } }
public class Sys { public static void Main() { BookShop bookShop = new BookShop(".NET Bookshop");
Book b1 = new Book("The Right Way","123456789", 19.90m); Book b2 = new Book("The .NET Stuff","987654321", 29.90m);
Notifier notify = new Notifier();
bookShop.AddBook = new AddBookEventHandler(notify.instance_AddBook);
Run the code and you will have the same results. Now uncomment the last statement in the instance_AddBook() method and run the application again.
Only the event-like mechanism fires for the first book added to the bookshop; it didn't fire for the next time a book is added. We were able to set the value of the Multicast delegate to null in the watcher class by casting the source object (which is the BookShop instance that fired the event) to BookShop, then assigned a null value to the property AddBook. By assigning a null value to the AddBook property we remove the other subscribed event handler methods, causing the event to stop functioning. Also now you can use the = instead of += operator to assign a delegate to the multicast delegate, which also is not valid because the event should adds on its multicast delegate, not replace it.
What we have done in the above application is remove the event from the BookShop class then create a private field of delegate type AddBookEventHandler and its public property. This property is used to assign a AddBookEventHandler delegate instance to the private field addBook. The method OnAddBook() raises the event by calling the delegate through the AddBook property. In the Sys class we assign a new AddBookEventHandler delegate instance to the AddBook property using the Assignment operator. The event is raised but because AddBook is a public property we have the ability to assign any new value to it outside the BookShop instance as we did in the event handler method and set null value to this property. So when we use this mechanism we define AddBook as a property.
But with the event model we define it as an event which has restrictions
As you can see, the event is very similar to the property that we created but with restrictions. To understand this better take the following statement and place it at the end of the Notifier class' event handler method of the event example (not the event without an event section) then try to compile the code.
((BookShop)source).AddBook = null;
The code will not compile and you will get the following error:
It says that you can't use the AddBook event except as the left hand side of += or -= operations outside the BookShop. I hope that by now you understand how events are created and what the difference is between using an event and a delegate.