Condividi tramite


Delegates and Events

This chapter is excerpted from Learning C# 3.0: Master the fundamentals of C# 3.0 by Jesse Liberty, Brian MacDonald, published by O'Reilly Media

Learning C# 3.0

Logo

Buy Now

When a head of state dies, the president of the United States sometimes does not have time to attend the funeral personally. Instead, he dispatches a delegate. Often this delegate is the vice president, but sometimes the VP is unavailable and the president must send someone else, such as the secretary of state or even the first lady. He does not want to "hardwire" his delegated authority to a single person; he might delegate this responsibility to anyone who is able to execute the correct international protocol.

The president defines in advance what responsibility will be delegated (attend the funeral), what items will be passed (condolences, kind words), and what value he hopes to get back (good will). He then assigns a particular person to that delegated responsibility at "runtime" as the course of his presidency progresses.

In programming, you are often faced with situations where you need to execute a particular action, but you don't know in advance which method, or even which object, you'll want to call upon to execute it. For example, you might want to tell an object to play a media file during runtime, but you might not know what object will be playing the file, or whether it's a video, a sound file, an animation, or something else. Rather than hardcoding a particular media player object, you would create a delegate, and then resolve that delegate to a particular method when the program executes.

In the early, dark, and primitive days of computing, a program would begin execution and then proceed through its steps until it completed. If the user was involved, the interaction was strictly controlled and limited to filling in fields. That's the model you've followed in all the console applications in this book so far, but it's not how you're used to interacting with applications these days.

Today's graphical user interface (GUI) programming model uses a different approach, known as event-driven programming. A modern program presents the user interface and waits for the user to take an action. The user might take many different actions, such as choosing among menu selections, pushing buttons, updating text fields, clicking icons, and so forth. Each action causes an event to be raised. Other events can be raised without direct user action, such as events that correspond to timer ticks of the internal clock, email being received, file-copy operations completing, and so forth.

An event is the encapsulation of the idea that "something happened" to which the program must respond. Events and delegates are tightly coupled concepts because flexible event handling requires that the response to the event be dispatched to the appropriate event handler. An event handler is typically implemented in C# via a delegate. Visual Studio does a lot of work for you in creating event handlers, but you should know how they work first, and then we'll show you Windows application programming in the next chapter.

Delegates

A delegate is a reference type, like the other reference types you've seen in this book, but instead of referring to an object, a delegate refers to a method. This is called encapsulating the method. When you create the delegate, you specify a method signature and return type; you can encapsulate any matching method with that delegate.

You create a delegate with the delegate keyword, followed by a return type and the signature of the methods that can be delegated to it, as in the following:

public delegate int FindResult(object obj1, object obj2);

This declaration defines a delegate named FindResult, which will encapsulate any method that takes two objects as parameters and that returns an int.

Once the delegate is defined, you can encapsulate a member method with that delegate by creating an instance of the delegate, passing in a method that matches the return type and signature. Notice that the delegate has no method body; that's because you're not defining the method here. You're simply saying that this delegate can encapsulate any method with the appropriate signature; you don't care what it does or how it does it, as long as it has the right parameters and returns the correct type.

As an alternative, you can use anonymous methods or lambda expressions, as described later in this chapter. In either case, you can use the delegate to invoke that encapsulated method.

Delegates decouple the class that declares the delegate from the class that uses the delegate. That's part of the principle of encapsulation that we talked about back in Chapter 6, Object-Oriented Programming. The class that declares the delegate FindResult doesn't need to know how the result is found, or what class uses the delegate; all it needs to do is get an int back.

For example, suppose you have a class called MediaStorage that you use to store and manage various media files-audio files, video files, animation files; the type of file doesn't matter to the class. Suppose further that you want this class to be able to play the files to make sure they can be played successfully, and report on whether they played properly or not (as a way of testing whether the file is valid). The MediaStorage class doesn't need to know how to play the files; it just needs to receive a code indicating whether the file played successfully or not.

You could fulfill this requirement with interfaces, although it may not be worth it to you to define an entirely new interface and create an instance of it when you could use a delegate instead. In this case, we'll be testing only two types of media files, so we'll use delegates. If there were a wider range of media file types, you might want to define an appropriate interface.

The delegate declaration in MediaStorage is rather simple:

public delegate int PlayMedia();

This delegate takes no parameters, but expects an int as a return value, to indicate whether the file played successfully. A value of 0 indicates success; anything else indicates failure. Note again that the method has no body.

The only other method in MediaStorage is ReportResult( ), which outputs to the console the result from the media test:

public void ReportResult(PlayMedia playerDelegate)
{
    if (playerDelegate( ) == 0)
    {
        Console.WriteLine("Media played successfully.");
    }
    else
    {
        Console.WriteLine("Media did not play successfully.");
    }
}

This looks like a normal method, except for the parameter it takes: playerDelegate, which is not an int, as you might expect, but rather a delegate, of type PlayMedia, which you declared earlier. It's not easy to think of a method in the same terms that you might normally think of an object, but that's how delegates work.

In the body of the method, you can't declare playerDelegate directly as an integer, because playerDelegate is a reference to a method. Instead, you evaluate the method that the delegate points to, and compare the result. That's why you're testing playerDelegate()==0. From there, you just output an appropriate message.

Take a look now at one of the media player classes:

public class AudioPlayer
{
    private int audioPlayerStatus;

    public int PlayAudioFile( )
    {
        Console.WriteLine("Simulating playing an audio file here.");
        audioPlayerStatus = 0;
        return audioPlayerStatus;
    }
}

This class has one private internal member, and a simple public method that simulates playing an audio file and returning a status code in the form of an int. This method, PlayAudioFile( ), has the signature the delegate requires, so this method can be used with the delegate. (Of course, a real media player would have many more methods than just this one, but we're keeping things simple for testing purposes.)

The other media player class is VideoPlayer, with a similar PlayVideoFile( ) method.

Within the body of the program, you first need to instantiate the MediaStorage class, and then one of each of the players:

MediaStorage myMediaStorage = new MediaStorage( );

AudioPlayer myAudioPlayer = new AudioPlayer( );
VideoPlayer myVideoPlayer = new VideoPlayer( );

That's easy enough. The next thing you need to do is instantiate the delegates. The delegates are of the type MediaStorage.PlayMedia (note that you're using the MediaStorage class here, not the object of that class you created a minute ago). You still use the keyword new to instantiate the delegate, but you pass the method PlayAudioFile as a parameter to the delegate when it's created. The result is that audioPlayerDelegate is a delegate of type PlayMedia, which you can now work with as a reference to that method:

// instantiate the delegates
MediaStorage.PlayMedia audioPlayerDelegate = new
                   MediaStorage.PlayMedia(myAudioPlayer.PlayAudioFile);
MediaStorage.PlayMedia videoPlayerDelegate = new 
                   MediaStorage.PlayMedia(myVideoPlayer.PlayVideoFile);

Now that you have the two delegate instances, you use the delegates with the ReportResult( ) method to see whether the media files were valid. Notice here that what you're passing to the ReportResult( ) method is a reference to a method in a different class altogether:

myMediaStorage.ReportResult(audioPlayerDelegate);
myMediaStorage.ReportResult(videoPlayerDelegate);

The outcome of this is the first line causes ReportResult( ) to call the PlayAudioFile( ) method, but the second line causes it to call the PlayVideoFile( ) method. At compile time, ReportResult( ) doesn't know which method it is going to call-it finds out only when it is invoked at runtime. All it needs to know is that any method it will be asked to call will match the signature defined by the PlayMedia delegate.

The full program is shown in Example 17.1, "Working with delegates seems complicated at first, but you just need to remember that you're passing a reference to a method", followed by the outcome.

Example 17.1. Working with delegates seems complicated at first, but you just need to remember that you're passing a reference to a method

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Example_17_1_ _ _ _Using_Delegates
{
    public class MediaStorage
    {
        public delegate int PlayMedia( );

        public void ReportResult(PlayMedia playerDelegate)
        {
            if (playerDelegate( ) == 0)
            {
                Console.WriteLine("Media played successfully.");
            }
            else
            {
                Console.WriteLine("Media did not play successfully.");
            }
        }
    }

    public class AudioPlayer
    {
        private int audioPlayerStatus;

        public int PlayAudioFile( )
        {
            Console.WriteLine("Simulating playing an audio file here.");
            audioPlayerStatus = 0;
            return audioPlayerStatus;
        }
    }

    public class VideoPlayer
    {
        private int videoPlayerStatus;

        public int PlayVideoFile( )
        {
            Console.WriteLine("Simulating a failed video file here.");
            videoPlayerStatus = -1;
            return videoPlayerStatus;
        }
    }

    public class Tester
    {
        public void Run( )
        {
            MediaStorage myMediaStorage = new MediaStorage( );

            // instantiate the two media players
            AudioPlayer myAudioPlayer = new AudioPlayer( );
            VideoPlayer myVideoPlayer = new VideoPlayer( );

            // instantiate the delegates
            MediaStorage.PlayMedia audioPlayerDelegate = new 
                   MediaStorage.PlayMedia(myAudioPlayer.PlayAudioFile);
            MediaStorage.PlayMedia videoPlayerDelegate = new 
                   MediaStorage.PlayMedia(myVideoPlayer.PlayVideoFile);

            // call the delegates
            myMediaStorage.ReportResult(audioPlayerDelegate);
            myMediaStorage.ReportResult(videoPlayerDelegate);
            
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Tester t = new Tester( );
            t.Run( );
        }
    }
}

Just for variety, the video player class returns an error code. Of course, you'd probably want your MediaStorage class to take more action than simply reporting that the file didn't play, but we won't go into that here. This is what the output looks like:

Simulating playing an audio file here.
Media played successfully.
Simulating a failed video file here.
Media did not play successfully.

Events

GUIs, such as Microsoft Windows and web browsers, require that programs respond to events. An event might be a button click, a menu selection, the completion of a file transfer, and so forth. In short, something happens and you must respond to it. You cannot predict the order in which events will arise. The system is quiescent until the event, and then springs into action to handle it.

In a GUI environment, any number of controls can raise an event. For example, when you click a button, it might raise the Click event. When you add to a drop-down list, it might raise a ListChanged event.

Other classes will be interested in responding to these events. How they respond is not of interest to the class raising the event. The button says, "I was clicked," and the responding classes react appropriately.

Publishing and Subscribing

In C#, any object can publish a set of events to which other classes can subscribe. When the publishing class raises an event, all the subscribed classes are notified. With this mechanism, your object can say, "Here are things I can notify you about," and other classes might sign up, saying, "Yes, let me know when that happens." For example, a button might notify any number of interested observers when it is clicked. The button is called the publisher because the button publishes the Click event and the other classes are the subscribers because they subscribe to the Click event. Note that the publishing class does not know or care who (if anyone) subscribes; it just raises the event. Who responds to that event, and how they respond, is not the concern of the publishing class.

Tip

This design implements the Publish/Subscribe (Observer) Pattern described in the seminal work Design Patterns by Erich Gamma et al. (Addison-Wesley).

As a second example, a Clock might notify interested classes whenever the time changes by one second. The Clock class could itself be responsible for the user interface representation of the time, rather than raising an event, so why bother with the indirection of using delegates? The advantage of the publish/subscribe idiom is that the Clock class doesn't need to know how its information will be used; this way, the monitoring of the time is decoupled from the representation of that information, just as in the previous example the request to play the media was decoupled from the details of the player itself. In addition, any number of classes can be notified when an event is raised. The subscribing classes do not need to know how the Clock works, and the Clock does not need to know what they are going to do in response to the event. The subscribing classes don't need to know about each other, either.

The publisher and the subscribers are decoupled by the delegate. This is highly desirable; it makes for more flexible and robust code. The Clock can change how it detects time without breaking any of the subscribing classes. The subscribing classes can change how they respond to time changes without breaking the Clock. Publishers and subscribers operate independently of one another, and that makes for code that is easier to maintain.

Events and Delegates

Events in C# are implemented with delegates. The publishing class defines a delegate. The subscribing class does two things: first, it creates a method that matches the signature of the delegate, and then it creates an instance of that delegate type encapsulating that method. When the event is raised, the subscribing class's methods are invoked through the delegate.

A method that handles an event is called an event handler. You can declare your event handlers as you would any other delegate.

By convention, event handlers in the .NET Framework always return void and take two parameters. The first parameter is the "source" of the event (that is, the publishing object). The second parameter is an object derived from EventArgs. Your event handlers will need to follow this design pattern.

EventArgs is the base class for all event data. Other than its constructor, the EventArgs class inherits all its methods from Object, though it does add a public static field named Empty, which represents an event with no state (to allow for the efficient use of events with no state). In other words, the EventArgs class is an empty bucket that you can use to supply any information you want about the event, or no information at all. What the subscribing class does with that information is the subscriber's business; it doesn't matter to the publisher. In this way, the subscribing class can easily match the required delegate signature, by simply taking a parameter of type EventArgs. The subscriber might use all, some, or none of the information passed in EventArgs; it doesn't matter.

Suppose you want to create a Clock class that uses delegates to notify potential subscribers whenever the local time changes its value by one second. Call this delegate SecondChangeHandler.

The declaration for the SecondChangeHandler delegate is:

public delegate void SecondChangeHandler(
    object clock, TimeInfoEventArgs timeInformation);

This delegate will encapsulate any method that returns void and that takes two parameters. The first parameter is an object that represents the Clock (the object raising the event), and the second parameter is an object of type TimeInfoEventArgs, derived from EventArgs, that will contain useful information for anyone interested in this event. TimeInfoEventArgs is defined as follows:

public class TimeInfoEventArgs : EventArgs
{
    public int hour;
    public int minute;
    public int second;
    public TimeInfoEventArgs(int hour, int minute, int second)
    {
        this.hour = hour;
        this.minute = minute;
        this.second = second;
    }
}

The TimeInfoEventArgs object will have information about the current hour, minute, and second. It defines a constructor and three public integer variables.

In addition to its delegate, the Clock class has three member variables-hour, minute, and second-as well as a single method, Run( ):

public void Run( )
{
    for (; ; )
    {
        // sleep 100 milliseconds
        Thread.Sleep(100);

        // get the current time
        System.DateTime dt = System.DateTime.Now;
        // if the second has changed
        // notify the subscribers
        if (dt.Second != second)
        {

            // create the TimeInfoEventArgs object
            // to pass to the subscriber
            TimeInfoEventArgs timeInformation = 
                 new TimeInfoEventArgs(dt.Hour, dt.Minute, dt.Second);

            // if anyone has subscribed, notify them
            if (SecondChanged != null)
            {
                SecondChanged(this, timeInformation);
            }
        }

        // update the state
        this.second = dt.Second;
        this.minute = dt.Minute;
        this.hour = dt.Hour;
    }
}

Run( ) creates an infinite for loop that periodically checks the system time. If the time has changed from the Clock object's current time, it notifies all of its subscribers and then updates its own state.

The first step is to sleep for 10 milliseconds:

Thread.Sleep(100);

This line uses a method you haven't seen yet, a static method of the Thread class from the System.Threading namespace. The Thread class is an advanced topic we won't cover in this book, but in this case, Thread.Sleep( ) simply serves the function of making your program check the clock every 100 milliseconds. Without the call to Sleep( ), your program would check the system clock so often that your processor couldn't do anything else. You also need to add usingSystem.Threading; to your using statements for Sleep( ) to work.

After sleeping for 100 milliseconds, the method checks the current time:

System.DateTime dt = System.DateTime.Now;

About every 10 times it checks, the second will have incremented. The method notices that change and notifies its subscribers. To do so, it first creates a new TimeInfoEventArgs object:

if (dt.Second != second)
{
    // create the TimeInfoEventArgs object
    // to pass to the subscriber
    TimeInfoEventArgs timeInformation = 
         new TimeInfoEventArgs(dt.Hour, dt.Minute, dt.Second);

It then notifies the subscribers by firing the SecondChanged event. SecondChanged is the instance of the delegate type SecondChangeHandler that was declared earlier in the class:

// if anyone has subscribed, notify them
if (SecondChanged != null)
{
    SecondChanged(this, timeInformation);
}

If an event has no subscribers registered, it will evaluate to null. The preceding test checks that the value is not null, ensuring that there are subscribers before calling SecondChanged.

Like all events, SecondChanged takes two arguments: the source of the event and the object derived from EventArgs. In the snippet, you see that the clock's this reference is passed because the clock is the source of the event. The second parameter is the TimeInfoEventArgs object, timeInformation, created in the preceding snippet.

Raising the event will invoke whatever methods have been registered with the Clock class through the delegate. We'll examine this in a moment.

Once the event is raised, you update the state of the Clock class:

this.second = dt.Second;
this.minute = dt.Minute;
this.hour = dt.Hour;

All that is left is to create classes that can subscribe to this event. You'll create two. First will be the DisplayClock class. The job of DisplayClock is not to keep track of time, but rather to display the current time to the console.

The example simplifies this class down to two methods. The first is a helper method named Subscribe( ) that is used to subscribe to the clock's SecondChanged delegate. The second method is the event handler TimeHasChanged( ):

public class DisplayClock
{
    public void Subscribe(Clock theClock)
    {
        theClock.SecondChanged += 
             new Clock.SecondChangeHandler(TimeHasChanged);
    }

    public void TimeHasChanged(object theClock, TimeInfoEventArgs ti)
    {
        Console.WriteLine("Current Time: {0}:{1}:{2}", 
          ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ));
    }
}

When the first method, Subscribe( ), is invoked, it creates a new SecondChangeHandler delegate, passing in its event handler method, TimeHasChanged( ). It then registers that delegate with the SecondChanged event of Clock. The += operator is the mechanism by which classes can register their event handlers with the event. As you'll see, using the += operator allows multiple classes to register handlers for a single event. Delegates with multiple subscribers are called multicast delegates.

You will create a second class that will also respond to this event, LogCurrentTime. This class would normally log the event to a file, but for our demonstration purposes, it will log to the standard console:

public class LogCurrentTime
{
    public void Subscribe(Clock theClock)
    {
        theClock.SecondChanged += 
              new Clock.SecondChangeHandler(WriteLogEntry);
    }

    public void WriteLogEntry(object theClock, TimeInfoEventArgs ti)
    {
        Console.WriteLine("Logging to file: {0}:{1}:{2}", 
           ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ));
    }
}

Although in this example these two classes are very similar, in a production program any number of disparate classes might subscribe to an event.

All that remains is to create a Clock class, create the DisplayClock class, and tell it to subscribe to the event. You then will create a LogCurrentTime class and tell it to subscribe as well. Finally, you'll tell the Clock to run. All of this is shown in Example 17.2, "You can implement events with delegates by setting up a publishing class with a delegate and subscribing classes that handle the event" (you'll need to press Ctrl-C to terminate this application).

Example 17.2. You can implement events with delegates by setting up a publishing class with a delegate and subscribing classes that handle the event

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Example_17_2_ _ _ _Delegates_and_Events
{
    // a class to hold the information about the event
    // in this case it will hold only information
    // available in the clock class, but could hold
    // additional state information
    public class TimeInfoEventArgs : EventArgs
    {
        public int hour;
        public int minute;
        public int second;

        public TimeInfoEventArgs(int hour, int minute, int second)
        {
            this.hour = hour;
            this.minute = minute;
            this.second = second;
        }
    }

    // The publisher: the class that other classes
    // will observe. This class publishes one delegate:
    // SecondChangeHandler.
    public class Clock
    {
        private int hour;
        private int minute;
        private int second;

        // the delegate the subscribers must implement
        public delegate void SecondChangeHandler(object clock, 
                             TimeInfoEventArgs timeInformation);

        // an instance of the delegate
        public SecondChangeHandler SecondChanged;

        // set the clock running
        // it will raise an event for each new second
        public void Run( )
        {
            for (; ; )
            {
                // sleep 100 milliseconds
                Thread.Sleep(100);
                // get the current time
                System.DateTime dt = System.DateTime.Now;
                // if the second has changed
                // notify the subscribers
                if (dt.Second != second)
                {
                    // create the TimeInfoEventArgs object
                    // to pass to the subscriber
                    TimeInfoEventArgs timeInformation = 
                         new TimeInfoEventArgs(dt.Hour, dt.Minute, dt.Second);

                    // if anyone has subscribed, notify them
                    if (SecondChanged != null)
                    {
                        SecondChanged(this, timeInformation);
                    }
                }

                // update the state
                this.second = dt.Second;
                this.minute = dt.Minute;
                this.hour = dt.Hour;
            }
        }
    }

    // A subscriber: DisplayClock subscribes to the
    // clock's events. The job of DisplayClock is
    // to display the current time
    public class DisplayClock
    {
        // given a clock, subscribe to
        // its SecondChangeHandler event
        public void Subscribe(Clock theClock)
        {
            theClock.SecondChanged += 
                 new Clock.SecondChangeHandler(TimeHasChanged);
        }

        // the method that implements the
        // delegated functionality
        public void TimeHasChanged(object theClock, TimeInfoEventArgs ti)
        {
            Console.WriteLine("Current Time: {0}:{1}:{2}", 
              ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ));
        }
    }
    // a second subscriber whose job is to write to a file
    public class LogCurrentTime
    {
        public void Subscribe(Clock theClock)
        {
            theClock.SecondChanged += 
                  new Clock.SecondChangeHandler(WriteLogEntry);
        }

        // this method should write to a file
        // we write to the console to see the effect
        // this object keeps no state
        public void WriteLogEntry(object theClock, TimeInfoEventArgs ti)
        {
            Console.WriteLine("Logging to file: {0}:{1}:{2}", 
               ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ));
        }
    }

    public class Tester
    {
        public void Run( )
        {
            // create a new clock
            Clock theClock = new Clock( );

            // create the display and tell it to
            // subscribe to the clock just created
            DisplayClock dc = new DisplayClock( );
            dc.Subscribe(theClock);

            // create a Log object and tell it
            // to subscribe to the clock
            LogCurrentTime lct = new LogCurrentTime( );
            lct.Subscribe(theClock);

            // Get the clock started
            theClock.Run( );
        }
    }

    public class Program
    {
        public static void Main( )
        {
            Tester t = new Tester( );
            t.Run( );
        }
    }
}

The output will look something like this, depending on what time it is when you run the program:

Current Time: 14:53:56
Logging to file: 14:53:56
Current Time: 14:53:57
Logging to file: 14:53:57
Current Time: 14:53:58
Logging to file: 14:53:58
Current Time: 14:53:59
Logging to file: 14:53:59
Current Time: 14:54:0
Logging to file: 14:54:0

The net effect of this code is to create two classes, DisplayClock and LogCurrentTime, both of which subscribe to a third class's event (Clock.SecondChanged).

SecondChanged is a multicast delegate field, initially referring to nothing. In time, it refers to a single delegate, and then later to multiple delegates. When the observer classes wish to be notified, they create an instance of the delegate and then add these delegates to SecondChanged. For example, in DisplayClock.Subscribe( ), you see this line of code:

theClock.SecondChanged +=
  new Clock.SecondChangeHandler(TimeHasChanged);

It turns out that the LogCurrentTime class also wants to be notified. In its Subscribe( ) method is very similar code:

public void Subscribe(Clock theClock)
{
  theClock.SecondChanged +=
    new Clock.SecondChangeHandler(WriteLogEntry);
}

Solving Delegate Problems with Events

There is a potential problem with Example 17.2, "You can implement events with delegates by setting up a publishing class with a delegate and subscribing classes that handle the event", however. What if the LogCurrentTime class was not so considerate, and it used the assignment operator (=) rather than the subscribe operator (+=), as in the following?

public void Subscribe(Clock theClock)
{
  theClock.SecondChanged =
    new Clock.SecondChangeHandler(WriteLogEntry);
}

If you make that one tiny change to the example, you'll find that the WriteLogEntry( ) method is called, but the TimeHasChanged( ) method is not called. The assignment operator replaced the delegate held in the SecondChanged multicast delegate. This is not good.

A second problem is that other methods can call SecondChangeHandler directly. For example, you might add the following code to the Run( ) method of your Tester class:

Console.WriteLine("Calling the method directly!");
System.DateTime dt = System.DateTime.Now.AddHours(2);
TimeInfoEventArgs timeInformation =
    new TimeInfoEventArgs(  dt.Hour,dt.Minute,dt.Second);
theClock.SecondChanged(theClock, timeInformation);

Here, Main( ) has created its own TimeInfoEventArgs object and invoked SecondChanged directly. This runs fine, even though it is not what the designer of the Clock class intended. Here is the output:

Calling the method directly!
Current Time: 18:36:27
Logging to file: 18:36:27
Current Time: 16:36:27
Logging to file: 16:36:27

The problem is that the designer of the Clock class intended the methods encapsulated by the delegate to be invoked only when the event is fired. Here, Main( ) has gone around through the back door and invoked those methods itself. What is more, it has passed in bogus data (passing in a time construct set to two hours into the future!).

How can you, as the designer of the Clock class, ensure that no one calls the delegated method directly? You can make the delegate private, but then it won't be possible for clients to register with your delegate at all. What's needed is a way to say, "This delegate is designed for event handling: you may subscribe and unsubscribe, but you may not invoke it directly."

The event Keyword

The solution to this dilemma is to use the event keyword. The event keyword indicates to the compiler that the delegate can be invoked only by the defining class, and that other classes can subscribe to and unsubscribe from the delegate using only the appropriate += and -= operators, respectively.

To fix your program, change your definition of SecondChanged from:

public SecondChangeHandler SecondChanged;

to the following:

public event SecondChangeHandler SecondChanged;

Adding the event keyword fixes both problems. Classes can no longer attempt to subscribe to the event using the assignment operator (=), as they could previously, nor can they invoke the event directly, as was done in the preceding example. Either of these attempts will now generate a compile error:

The event 'Example_17_3_ _Delegates_and_events.Clock.SecondChanged' 
can only appear on the left-hand side of += or -= (except when used 
from within the type 'Example_17_3_  _Delegates_and_events.Clock')

There are two ways of looking at SecondChanged now that you've modified it. In one sense, it is simply a delegate instance to which you've restricted access using the keyword event. In another, more important sense, SecondChangedis an event, implemented by a delegate of type SecondChangeHandler. These two statements mean the same thing, but the latter is a more object-oriented way of looking at it, and better reflects the intent of this keyword: to create an event that your object can raise, and to which other objects can respond.

The complete source, modified to use the event rather than the unrestricted delegate, is shown in Example 17.3, "Using the event keyword turns your delegate into an event, and restricts other classes' ability to interact with it to subscribing or unsubscribing".

Example 17.3. Using the event keyword turns your delegate into an event, and restricts other classes' ability to interact with it to subscribing or unsubscribing

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Example_17_3_ _ _ _Delegates_and_Events
{
    // a class to hold the information about the event
    // in this case it will hold only information
    // available in the clock class, but could hold
    // additional state information
    public class TimeInfoEventArgs : EventArgs
    {
        public int hour;
        public int minute;
        public int second;

        public TimeInfoEventArgs(int hour, int minute, int second)
        {
            this.hour = hour;
            this.minute = minute;
            this.second = second;
        }
    }

    // The publisher: the class that other classes
    // will observe. This class publishes one delegate:
    // SecondChanged.
    public class Clock
    {
        private int hour;
        private int minute;
        private int second;

        // the delegate the subscribers must implement
        public delegate void SecondChangeHandler(object clock, 
                        TimeInfoEventArgs timeInformation);
        // an instance of the delegate with event keyword added
        public event SecondChangeHandler SecondChanged;
        // set the clock running
        // it will raise an event for each new second
        public void Run( )
        {
            for (; ; )
            {
                // sleep 10 milliseconds
                Thread.Sleep(100);

                // get the current time
                System.DateTime dt = System.DateTime.Now;
                // if the second has changed
                // notify the subscribers
                if (dt.Second != second)
                {
                    // create the TimeInfoEventArgs object
                    // to pass to the subscriber
                    TimeInfoEventArgs timeInformation = 
                        new TimeInfoEventArgs(dt.Hour, dt.Minute, dt.Second);

                    // if anyone has subscribed, notify them
                    if (SecondChanged != null)
                    {
                        SecondChanged(this, timeInformation);
                    }
                }

                // update the state
                this.second = dt.Second;
                this.minute = dt.Minute;
                this.hour = dt.Hour;
            }
        }
    }

    // A subscriber: DisplayClock subscribes to the
    // clock's events. The job of DisplayClock is
    // to display the current time
    public class DisplayClock
    {
        // given a clock, subscribe to
        // its SecondChangeHandler event
        public void Subscribe(Clock theClock)
        {
            theClock.SecondChanged += 
                     new Clock.SecondChangeHandler(TimeHasChanged);
        }

        // the method that implements the
        // delegated functionality
        public void TimeHasChanged(object Clock, TimeInfotheEventArgs ti)
        {
            Console.WriteLine("Current Time: {0}:{1}:{2}", 
              ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ));
        }
    }
    // a second subscriber whose job is to write to a file
    public class LogCurrentTime
    {
        public void Subscribe(Clock theClock)
        {
            theClock.SecondChanged += 
                     new Clock.SecondChangeHandler(WriteLogEntry);
        }

        // this method should write to a file
        // we write to the console to see the effect
        // this object keeps no state
        public void WriteLogEntry(object theClock, TimeInfoEventArgs ti)
        {
            Console.WriteLine("Logging to file: {0}:{1}:{2}", 
              ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ));
        }
    }

    public class Tester
    {
        public void Run( )
        {
            // create a new clock
            Clock theClock = new Clock( );

            // create the display and tell it to
            // subscribe to the clock just created
            DisplayClock dc = new DisplayClock( );
            dc.Subscribe(theClock);

            // create a Log object and tell it
            // to subscribe to the clock
            LogCurrentTime lct = new LogCurrentTime( );
            lct.Subscribe(theClock);

            // Get the clock started
            theClock.Run( );
        }
    }

    public class Program
    {
        public static void Main( )
        {
            Tester t = new Tester( );
            t.Run( );
        }
    }
}

Using Anonymous Methods

In the previous example, you subscribed to the event by invoking a new instance of the delegate, passing in the name of a method that implements the event:

theClock.SecondChanged += 
         new Clock.SecondChangeHandler(TimeHasChanged);

You can also assign this delegate by writing the shortened version:

theClock.SecondChanged += TimeHasChanged;

Later in the code, you must define TimeHasChanged as a method that matches the signature of the SecondChangeHandler delegate:

public void TimeHasChanged(object theClock, TimeInfoEventArgs ti)
{
    Console.WriteLine("Current Time: {0}:{1}:{2}",
      ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ));
}

Anonymous methods allow you to pass a code block rather than the name of the method. This can make for code that is more efficient and easier to maintain, and the anonymous method has access to the variables in the scope in which they are defined.

clock.SecondChanged += delegate( object theClock, TimeInfoEventArgs ti )
{
    Console.WriteLine( "Current Time: {0}:{1}:{2}",
      ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ) );
};

Notice that rather than registering an instance of a delegate, you use the keyword delegate, followed by the parameters that would be passed to your method, followed by the body of your method encased in braces and terminated by a semicolon.

This method has no name; hence, it is anonymous. You cannot invoke the method except through the delegate; but that is exactly what you want.

Lambda Expressions

C# 3.0 extends the concept of anonymous methods and introduces lambda expressions, which are more powerful and flexible than anonymous methods.

Tip

Lambda expressions get their name from lambda calculus, which is a complicated topic, but in a nutshell, it's a mathematical notation for describing functions. That's pretty much what's going on here; lambda expressions describe methods without naming them.

You define a lambda expression using this syntax:

(input parameters) => {expression or statement block};

The lambda operator => is newly introduced in C# 3.0 and is read as "goes to". The left operand is a list of zero or more input parameters, and the right operand is the body of the lambda expression. Notice that => is an operator, which means that the preceding line of code is an expression. Just as x+x is an expression that returns a value-if x is 2, the expression returns the int value 4-a lambda expression is an expression that returns a method. It's not a method by itself. It's tricky to think of expressions as returning a method instead of a value, but at the beginning of this chapter, you wouldn't have thought of passing a method as a parameter, either.

You can thus rewrite the delegate definition as follows:

theClock.OnSecondChange +=
    (aClock, ti) =>
    {
        Console.WriteLine("Current Time: {0}:{1}:{2}",
                           ti.hour.ToString( ),
                           ti.minute.ToString( ),
                           ti.second.ToString( ));
    };

You read this as "theClock's OnSecondChange delegate adds an anonymous delegate defined by this lambda expression." The two parameters, aClock and ti, go to the WriteLine expression that writes out the hour and minute and second from ti.

The two input parameters, aClock and ti, are of type Clock and TimeInfoEventArgs, respectively. You don't need to specify their types, because the C# compiler infers their type from the OnSecondChange delegate definition. If the compiler is unable to infer the type of your operands, you may specify them explicitly, like this:

(Clock aClock, TimeInfoEventArgs ti) => {...};

You might also want to specify the types of the input parameters to make the code more readable.

If your method doesn't have any input parameters, you write a pair of empty parentheses, like this:

( ) => {Console.WriteLine("No parameters here."}};

If you have only one input parameter, you can skip the parentheses:

n => {n * n};

Finally, if your method has only one statement, you can skip the braces as well:

n => n * n

So, what's the difference between lambda expressions and anonymous methods? Anonymous methods were introduced in C# 2.0 specifically to deal with situations where you didn't want to define a method for a delegate; that's why anonymous methods use the delegate keyword, and can be used only in the context of delegates. Lambda expressions were introduced in C# 3.0 to take that idea further. Specifically, lambda expressions were introduced to work with LINQ, the Language Integrated Query, which has to do with handling data. You'll see more about LINQ in Chapter 21, LINQ. For now, you can use lambda expressions anywhere you'd want to use an anonymous method.

Summary

  • Modern GUIs rely on events generated by the user or by the system to know what action to take.

  • A delegate is a reference to a method of a particular signature and return type.

  • Delegates allow polymorphism by encapsulating a method that matches the delegate signature. The method encapsulated by the delegate is decided at runtime.

  • An object can publish a series of events to which other classes can subscribe. The publishing class defines a delegate and an event based on that delegate. The subscribing class creates a method that matches the signature of the delegate, and registers that method with an instance of the delegate.

  • In .NET, all event handlers return void, and take two parameters. The first parameter is of type object and is the object that raises the event; the second argument is an object of type EventArgs or of a type derived from EventArgs, which may contain useful information about the event.

  • Event handlers subscribe to delegates using the += operator and unsubscribe using the -= operator.

  • The keyword event ensures that event handlers can only subscribe or unsubscribe to the event. Handlers can't call the delegate event directly, nor can they access its internal members.

  • Instead of passing a method name to a delegate, you can pass a block of code, using the keyword delegate. This creates an anonymous method.

  • A lambda expression is an expression using the operator => that returns an unnamed method. Lambda expressions are similar to anonymous methods, but aren't restricted to being used as delegates.

This chapter introduced a lot of new ideas, and possibly made you think differently about how methods work. A lot of what delegates do can also be done with interfaces, but as you've seen, delegates really come into their own as event handlers. Throughout this chapter, we've emphasized that one of the main functions of event handlers is to work with a GUI interface, like Windows, but we haven't shown you how to do that yet. In the next chapter, you're finally going to break out of the console window and see how to make some Windows applications. This is the one you've been waiting for.

Test Your Knowledge: Quiz

Question 17-1. What is the purpose of a delegate?

Question 17-2. Are delegates value types or reference types?

Question 17-3. Suppose a Phone class has defined an OnPhoneRings delegate. How would you instantiate a delegate called myDelegate to refer to the myMethod method?

Question 17-4. Define the OnPhoneRings delegate from the preceding question as an event to signal that the phone has rung.

Question 17-5. Give an example of how you might call the delegated method from the previous question through the delegate.

Question 17-6. What does the event keyword do?

Question 17-7. How do you pass information into the method that is called through the event?

Question 17-8. What properties or methods does System.EventArgs have?

Question 17-9. How can you create delegated methods anonymously?

Question 17-10. What is returned by a lambda expression?

Test Your Knowledge: Exercises

Exercise 17-1. Write a countdown alarm program that uses delegates to notify anyone who is interested that the designated amount of time has passed. You'll need a class to simulate the countdown clock that accepts a message and a number of seconds to wait (supplied by the user). After waiting the appropriate amount of time, the countdown clock should call the delegate and pass the message to any registered observers. (When you're calculating the time to wait, remember that Thread.Sleep( ) takes an argument in milliseconds, and requires a usingSystem.Threading statement.) Create an observer class as well that echoes the received message to the console.

Exercise 17-2. Change the program you wrote in Exercise 17-1 to ensure that the event can be published to multiple handlers safely.

Exercise 17-3. Rewrite the observer class in Exercise 17-2 to use an anonymous method.

Exercise 17-4. Rewrite the observer class in Exercise 17-3 to use a lambda expression instead of an anonymous method.