Events


In this section, you'll look at one of the most frequently used OOP techniques in .NET: events.

You'll start, as usual, with the basics — looking at what events actually are. After this, you'll move on to see some simple events in action and look at what you can do with them. Once this is described, you'll move on to look at how you can create and use events of your own.

At the end of this chapter you'll polish off your CardLib class library by adding an event. In addition, and since this is the last port of call before hitting some more advanced topics, you'll have a bit of fun. You'll create a card game application that uses this class library.

To start with, then, here's a look at what events are.

What Is an Event?

Events are similar to exceptions in that they are raised (thrown) by objects, and you can supply code that acts on them. However, there are several important differences. The most important of these is that there is no equivalent to the try . . . catch structure for handling events. Instead, you must subscribe to them. Subscribing to an event means supplying code that will be executed when an event is raised, in the form of an event handler.

An event can have many handlers subscribed to it, which will all be called when the event is raised. This may include event handlers that are part of the class of the object that raises the event, but event handlers are just as likely to be found in other classes.

Event handlers themselves are simply functions. The only restriction on an event handler function is that it must match the signature (return type and parameters) required by the event. This signature is part of the definition of an event and is specified by a delegate.

Note

The fact that delegates are used in events is what makes delegates such useful things. This is the reason I devoted some time to them back in Chapter 6, and you may wish to reread that section to refresh your memory as to what delegates are and how you use them.

The sequence of processing goes something like this.

First, an application creates an object that can raise an event. As an example, say that the application is an instant messaging application, and that the object it creates represents a connection to a remote user. This connection object might raise an event, say, when a message arrives through the connection from the remote user. This is shown in Figure 13-2.

image from book
Figure 13-2

Next, the application subscribes to the event. Your instant messaging application would do this by defining a function that could be used with the delegate type specified by the event and passing a reference to this function to the event. This event handler function might be a method on another object, for example an object representing a display device to display instant messages on when they arrive. This is shown in Figure 13-3.

image from book
Figure 13-3

When the event is raised, the subscriber is notified. When an instant message arrives through the connection object, the event handler method on the display device object is called. As you are using a standard method, the object that raises the event may pass any relevant information via parameters, making events very versatile. In the example case, one parameter might be the text of the instant message, which the event handler could display on the display device object. This is shown in Figure 13-4.

image from book
Figure 13-4

Using Events

In this section, you'll look at the code required for handling events, then move on to look at how you can define and use your own events.

Handling Events

As I have discussed, to handle an event you need to subscribe to the event by providing an event handler function whose signature matches that of the delegate specified for use with the event. Here's an example that uses a simple timer object to raise events, which will result in a handler function being called.

Try It Out – Handling Events

image from book
  1. Create a new console application called Ch13Ex01 in the directory C:\BegVCSharp\Chapter13.

  2. Modify the code in Program.cs as follows:

    using System; using System.Collections.Generic; using System.Text; using System.Timers;     namespace Ch13Ex01 {    class Program    { static int counter = 0; static string displayString = "This string will appear one letter at a time. ";       static void Main(string[] args)       { Timer myTimer = new Timer(100); myTimer.Elapsed += new ElapsedEventHandler(WriteChar); myTimer.Start(); Console.ReadKey();       } static void WriteChar(object source, ElapsedEventArgs e) { Console.Write(displayString[counter++ % displayString.Length]); }    } }

  3. Run the application (once running, pressing a key will terminate the application). The result, after a short period, is shown in Figure 13-5.

    image from book
    Figure 13-5

How It Works

The object you are using to raise events is an instance of the System.Timers.Timer class. This object is initialized with a time period (in milliseconds). When the Timer object is started using its Start() method a stream of events will be raised, spaced out in time according to the specified time period. Main() initializes a Timer object with a timer period of 100 milliseconds, so it will raise events 10 times a second when started:

static void Main(string[] args) {    Timer myTimer = new Timer(100);

The Timer object possesses an event called Elapsed, and the event handler signature required by this event is that of the System.Timers.ElapsedEventHandler delegate type, which is one of the standard delegates defined in the .NET Framework. This delegate is used for functions that match the following signature:

 void functionName(object source, ElapsedEventArgs e); 

The Timer object sends a reference to itself in the first parameter and an instance of an ElapsedEventArgs object in its second parameter. It is safe to ignore these parameters for now, but you'll take a look at them a little later.

In your code you have a method that matches this signature:

 static void WriteChar(object source, ElapsedEventArgs e) {    Console.Write(displayString[counter++ % displayString.Length]); }

This method uses the two static fields of Class1, counter and displayString, to display a single character. Every time the method is called the character displayed is different.

The next task is to hook this handler up to the event — to subscribe to it. To do this, you use the += operator to add a handler to the event in the form of a new delegate instance initialized with your event handler method:

static void Main(string[] args) {    Timer myTimer = new Timer(100); myTimer.Elapsed += new ElapsedEventHandler(WriteChar); 

This command (which uses slightly strange looking syntax, specific to delegates) adds a handler to the list that will be called when the Elapsed event is raised. You can add as many handlers as you like to this list, as long as they all meet the criteria required. Each handler will be called in turn when the event is raised.

All that is left for Main() is to start the timer running:

myTimer.Start(); 

Since you don't want the application terminating before you have handled any events, you then put the Main() function on hold. The simplest way of doing this is to request user input, since this command won't finish processing until the user has pressed a key.

Console.ReadKey();

Although processing in Main() effectively ceases here, processing in the Timer object continues. When it raises events it calls the WriteChar() method, which runs concurrently with the Console.ReadLine() statement.

image from book

Defining Events

Next, you'll look at defining and using your own events. In the following Try It Out, you implement an example version of the instant messaging case set out in the introduction to events in this chapter and create a Connection object that raises events that are handled by a Display object.

Try It Out – Defining Events

image from book
  1. Create a new console application called Ch13Ex02 in the directory C:\BegVCSharp\Chapter13.

  2. Add a new class, Connection, stored in Connection.cs:

    using System; using System.Collections.Generic; using System.Text; using System.Timers;     namespace Ch13Ex02 { public delegate void MessageHandler(string messageText);        public class Connection    { public event MessageHandler MessageArrived; private Timer pollTimer;       public Connection()       { pollTimer = new Timer(100); pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage);       } public void Connect() { pollTimer.Start(); } public void Disconnect() { pollTimer.Stop(); } private static Random random = new Random(); private void CheckForMessage(object source, ElapsedEventArgs e) { Console.WriteLine("Checking for new messages."); if ((random.Next(9) == 0) && (MessageArrived != null)) { MessageArrived("Hello Mum!"); } }    } }

  3. Add a new class, Display, stored in Display.cs:

    namespace Ch13Ex02 {    public class Display    { public void DisplayMessage(string message) { Console.WriteLine("Message arrived: {0}", message); }    } }
  4. Modify the code in Program.cs as follows:

    static void Main(string[] args) { Connection myConnection = new Connection(); Display myDisplay = new Display(); myConnection.MessageArrived += new MessageHandler (myDisplay.DisplayMessage); myConnection.Connect(); Console.ReadKey(); }
  5. Run the application. The result, after a short period, is shown in Figure 13-6.

    image from book
    Figure 13-6

How It Works

The class that does most of the work in this application is the Connection class. Instances of this class make use of a Timer object much like the one you saw in the first example of this chapter, initializing it in the class constructor and giving access to its state (enabled or disabled) via Connect() and Disconnect():

public class Connection {    private Timer pollTimer;        public Connection()    {       pollTimer = new Timer(100);       pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage);    }        public void Connect()    {       pollTimer.Start();    }    public void Disconnect()    {       pollTimer.Stop();    }        ... }

Also in the constructor, you register an event handler for the Elapsed event in the same way as you did in the first example. The handler method, CheckForMessage(), will raise an event on average once every 10 times it is called. Before you look at the code for this, though, it would be useful to look at the event definition itself.

However, before you define an event you must define a delegate type to use with the event — that is, a delegate type that specifies the signature that an event handling method must conform to. You do this using standard delegate syntax, defining it as public inside the Ch13Ex02 namespace in order to make the type available to external code:

namespace Ch13Ex02 { public delegate void MessageHandler(string messageText); 

This delegate type, called MessageHandler here, is a signature for a void function that has a single string parameter. You can use this parameter to pass an instant message received by the Connection object to the Display object.

Once a delegate has been defined (or a suitable existing delegate has been located), you can define the event itself, as a member of the Connection class:

public class Connection { public event MessageHandler MessageArrived; 

You simply name the event (here you have used the name MessageArrived) and declare it using the event keyword and the delegate type to use (the MessageHandler delegate type defined earlier).

Once you have declared an event in this way, you can raise it simply by calling it by its name as if it were a method with the signature specified by the delegate. For example, you could raise this event using:

 MessageArrived("This is a message."); 

If the delegate had been defined without any parameters you could use simply:

 MessageArrived(); 

Alternatively, you could have defined more parameters, which would have required more code to raise the event.

The CheckForMessage() method looks like this:

private static Random random = new Random();     private void CheckForMessage(object source, ElapsedEventArgs e) {    Console.WriteLine("Checking for new messages.");    if ((random.Next(9) == 0) && (MessageArrived != null))    {       MessageArrived("Hello Mum!");    } }

You use an instance of the Random class that you have seen in earlier chapters to generate a random number between 0 and 9, and raise an event if the number generated is 0, which should happen 10 percent of the time. This simulates polling the connection to see if a message has arrived, which won't be the case every time you check. To separate the timer from the instance of Connection, you use a private static instance of the Random class.

Note that you supply additional logic. You only raise an event if the expression MessageArrived != null evaluates to true. This expression, which again uses the delegate syntax in a slightly unusual way, means: "Does the event have any subscribers?" If there are no subscribers, MessageArrived evaluates to null, and there is no point in raising the event.

The class that will subscribe to the event is called Display and contains the single method, DisplayMessage(), defined as follows:

public class Display {    public void DisplayMessage(string message)    {       Console.WriteLine("Message arrived: {0}", message);    } }

This method matches the delegate type method signature (and is public, which is a requirement of event handlers in classes other than the class that generates the event), so you can use it to respond to the MessageArrived event.

All that is left now is for the code in Main() to initialize instances of the Connection and Display classes, hook them up, and start things going. The code required here is similar to that from the first example:

static void Main(string[] args) {    Connection myConnection = new Connection();    Display myDisplay = new Display();    myConnection.MessageArrived +=            new MessageHandler(myDisplay.DisplayMessage);    myConnection.Connect();    Console.ReadKey(); }

Again, you call Console.ReadKey() to pause the processing of Main() once you have started things moving with the Connect() method of the Connection object.

image from book

Multipurpose Event Handlers

The signature you saw earlier, for the Timer.Elapsed event, contained two parameters that are of a type often seen in event handlers. These parameters are:

  • object source: A reference to the object that raised the event

  • ElapsedEventArgs e: Parameters sent by the event

The reason that the object type parameter is used in this event, and indeed in many other events, is that you will often want to use a single event handler for several identical events generated by different objects and still tell which object generated the event.

To explain and illustrate this, I'll extend the last example a little.

Try It Out – Using a Multipurpose Event Handler

image from book
  1. Create a new console application called Ch13Ex03 in the directory C:\BegVCSharp\ Chapter13.

  2. Copy the code across for Program.cs, Connection.cs, and Display.cs from Ch13Ex02, making sure that you change the namespaces in each file from Ch13Ex02 to Ch13Ex03.

  3. Add a new class, MessageArrivedEventArgs, stored in MessageArrivedEventArgs.cs:

    namespace Ch13Ex03 { public class MessageArrivedEventArgs : EventArgs    { private string message; public string Message { get { return message; } }       public MessageArrivedEventArgs()       { message = "No message sent.";       }     public MessageArrivedEventArgs(string newMessage) { message = newMessage; }    } }
  4. Modify Connection.cs as follows:

    namespace Ch13Ex03 { public delegate void MessageHandler(Connection source, MessageArrivedEventArgs e);        public class Connection    {       public event MessageHandler MessageArrived;     private string name; public string Name { get { return name; } set { name = value; } }           ...           private void CheckForMessage(object source, EventArgs e)       {          Console.WriteLine("Checking for new messages.");          if ((random.Next(9) == 0) && (MessageArrived != null))          { MessageArrived(this, new MessageArrivedEventArgs("Hello Mum!"));          }       }           ...        } }

  5. Modify Display.cs as follows:

     public void DisplayMessage(Connection source, MessageArrivedEventArgs e) { Console.WriteLine("Message arrived from: {0}", source.Name); Console.WriteLine("Message Text: {0}", e.Message); }

  6. Modify Program.cs as follows:

    static void Main(string[] args) { Connection myConnection1 = new Connection(); myConnection1.Name = "First connection."; Connection myConnection2 = new Connection(); myConnection2.Name = "Second connection.";    Display myDisplay = new Display(); myConnection1.MessageArrived += new MessageHandler(myDisplay.DisplayMessage); myConnection2.MessageArrived += new MessageHandler(myDisplay.DisplayMessage); myConnection1.Connect(); myConnection2.Connect();    Console.ReadKey(); }

  7. Run the application. The result, after a short period, is shown in Figure 13-7.

    image from book
    Figure 13-7

How It Works

By sending a reference to the object that raises an event as one of the event handler parameters you can customize the response of the handler to individual objects. The reference gives you access to the source object, including its properties.

By sending parameters that are contained in a class that inherits from System.EventArgs (as ElapsedEventArgs does), you can supply whatever additional information necessary as parameters (such as the Message parameter on the MessageArrivedEventArgs class).

In addition, these parameters will benefit from polymorphism. You could define a handler for the MessageArrived event such as

 public void DisplayMessage(object source, EventArgs e) { Console.WriteLine("Message arrived from: {0}", ((Connection)source).Name); Console.WriteLine("Message Text: {0}", ((MessageArrivedEventArgs)e).Message); }

and modify the delegate definition in Connection.cs as follows:

 public delegate void MessageHandler(object source, EventArgs e); 

The application will execute exactly as it did before, but you have made the DisplayMessage() function more versatile (in theory at least — more implementation would be needed to make this production quality). This same handler could work with other events, such as the Timer.Elapsed, although you'd have to modify the internals of the handler a bit more such that the parameters sent when this event is raised are handled properly (casting them to Connection and MessageArrivedEventArgs objects in this way will cause an exception; you should use the as operator instead and check for null values).

image from book

Return Values and Event Handlers

All the event handlers you've seen so far have had a return type of void. It is possible to provide a return type for an event, but this can lead to problems. This is because a given event may result in several event handlers being called. If all of these handlers return a value, this leaves you in some doubt as to which value was actually returned.

The system deals with this by only allowing you access to the last value returned by an event handler. This will be the value returned by the last event handler to subscribe to an event.

Perhaps this functionality might be of use in some situations, although I can't think of one off the top of my head. I'd recommend using void type event handlers, as well as avoiding out type parameters.

Anonymous Methods

One more new capability in C# 2.0 is the ability to use anonymous methods as delegates. An anonymous method is one that doesn't actually exist as a method in the traditional sense, that is, it isn't a method on any particular class. Instead, an anonymous method is created purely for use as a target for a delegate.

To create a delegate that targets an anonymous method, you need the following code:

 delegate(parameters) { // Anonymous method code  }; 

Here, parameters is a list of parameters matching the delegate type you are instantiating, as used by the anonymous method code, for example:

 delegate(Connection source, MessageArrivedEventArgs e) {    // Anonymous method code matching MessageHandler event in Ch13Ex03. };

So, you could use this code to completely bypass the Display,DisplayMessage() method in Ch13Ex03:

 myConnection1.MessageArrived += delegate(Connection source, MessageArrivedEventArgs e) {    Console.WriteLine("Message arrived from: {0}", source.Name);    Console.WriteLine("Message Text: {0}", e.Message); };

An interesting point to note concerning anonymous methods is that they are effectively local to the code block that contains them, and they have access to local variables in this scope. If you use such a variable it becomes an outer variable. Outer variables are not disposed of when they go out of scope like other local variables are; instead, they live on until the anonymous methods that use them are destroyed. This may be some time later than you expect and is definitely something to be careful about!




Beginning Visual C# 2005
Beginning Visual C#supAND#174;/sup 2005
ISBN: B000N7ETVG
EAN: N/A
Year: 2005
Pages: 278

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net