Professor Mustard Programming
 

Programming 101 - Unit 06 - Other Features

06g) Events
 
You may have noticed that, up until now, communication between objects and the code that created them is rather one-sided. Granted, if an object's method has a return type, the object can use it pass some results back to the original program. Still, it's always the original program that has to take the initiative, by calling the appropriate methods in the object. Now that we've learned about delegates, however, we can write objects that are able to communicate with their owners directly, without any outside prompting!

Consider a program that allows you to continually stack weights onto a boat. Class1 would create a Boat object, and continually call the boat's AddBrick() method to add weight to it. The problem is that the Boat object has no way of warning Class1 that it's about to sink! We need some sort of delegate that the Boat object can use to send warning messages to Class1, as the boat gets progressively heavier. Let's start by declaring a delegate...

public delegate void BrickEventHandler(string message);


Nothing new here. We've defined a BrickEventHandler delegate with a return type of void, and one parameter of type string. The next thing we need is the Boat class which uses this delegate:

public class Boat
{
  private int _brickCount;
  public event BrickEventHandler BrickAdded;

  public Boat()
  {
    _brickCount = 0;
  }

  public void AddBrick()
  {
    ++_brickCount;
  }
}



This simple class contains one private member, _brickCount, which starts at 0, and one public method, AddBrick(), which increments _brickCount. We can also see that the class has an instance of our delegate, called BrickAdded... but wait! What's that "event" keyword do? Well, in case you haven't noticed, our BrickAdded delegate is public, which would normally mean that any code in our entire program can muck around with it. The problem is that we can't hide BrickAdded with protected or private, because the whole point of this exercise is to let our class communicate with the rest of the program. This necessitates a public delegate. The solution is to add the "event" keyword to our BrickAdded delegate instance, which basically means "use, but don't touch" to the rest of your program. The rest of your program can still use the += operator to add their own methods into the BrickAdded delegate (and even -= to take methods away), but the delegate itself is strictly hands-off.

Next, we need to initialize BrickAdded in Boat's constructor, so that we can use it. Since every delegate needs to be initialized with a method, and we don't want our delegate to be dependant on any methods outside of its class, we'll have to use a method from the actual Boat class. We don't need to put any code in the method (although we certainly can if we want to); it just has to match the BrickEventHandler delegate's signature. Remember, this method will be called every time the BrickAdded delegate is called, so only add code to this method if you want the class doing something internally whenever BrickAdded fires.

public Boat()
{
  _brickCount = 0;
  BrickAdded = new BrickEventHandler(OnBrickAdded);
}

private void OnBrickAdded(string message)
{
  // No code for now
}



The last thing we need to do in the Boat class is actually call the BrickAdded delegate, which we'll naturally do from the AddBrick() method:

public void AddBrick()
{
  ++_brickCount;
  BrickAdded("A brick has added; the brick count is now " + _brickCount.ToString());
}



Once again, this is just an example. We're only making a call to BrickAdded in AddBrick(), because it's the only place in the class where it makes sense to call the delegate. If there were other places in the Boat class that involved adding bricks, we would call the BrickAdded delegate in those places, as well.

Now that we've finished with the Boat class (and its BrickAdded event), all we need is a program that uses it:

[STAThread]
static void Main(string[] args)
{
  Boat = new boat();
}

static void boat_BrickAdded(string message)
{
  Console.WriteLine("Caught BrickAdded event! Message = " + message);
}



Notice how the program's boat_BrickAdded() method matches the BrickEventHandler signature? This is no coincidence... that's the method that's going to "subscribe" to our Boat object's BrickAdded event! Simply add one more line, and the deed is done:

[STAThread]
static void Main(string[] args)
{
  Boat = new boat();
  boat.BrickAdded += new BrickEventHandler(boat_BrickAdded);
}



Now, every time the AddBrick() method in the boat object is called, our boat_BrickAdded() method will be called by the boat object's BrickAdded event! In this manner, our boat object can send messages back to our main class, without any prompting from us! Of course, right now the boat object is just firing the BrickAdded event unconditionally, on every AddBrick() call. So at the moment, this isn't any more helpful than if we re-programmed AddBrick() to return a string. But when you consider that we can recode AddBrick() to only fire the event when a certain number of bricks is reached (which is exactly what we'll do in today's code sample), or make the event fire anywhere we want in Boat's code, you start to see a little more purpose in this exercise.

Remember, too, that we don't need to limit ourselves to strings. We can have the delegate deal with any native or user-defined datatype we want, and use as many parameters as we want. The whole point here is that our objects can now communicate back to the code that created them, without being asked. This is especially useful when you have a central class than contains many other objects. The objects can "report back" to the central class, and feed it information that it needs to do its job. The main class can even relay this information to the other objects, if it needs to! It's also great when you have an object full of data, and several displays which are dependant on this data. You can use events to directly update all the displays, whenever the object's data changes! Events open up many new possibilities in programming architecture and design.

An additional note: for an inherited class to fire one of its parent's events in the same manner that the base class would, it must override the event that was defined in the parent. As with methods, this requires making the parent's event virtual.



using System;

namespace G_Events
{
  class Class1
  {
    static readonly string ln = Environment.NewLine;

    [STAThread]
    static void Main(string[] args)
    {
      Boat boat = new Boat();

      // In effect, we "subscribe" to the boat object's BrickAdded
      // event. We say that everytime the BrickAdded event occurs
      // in the boat object, Class1's boat_BrickAdded method should
      // be called with the same information passed into it.
      boat.BrickAdded += new BrickEventHandler(boat_BrickAdded);

      for(int i = 0; i < 10; ++i)
        boat.AddBrick();
      
      PromptForExit();
    }

    // As always, we can name the "subscribing" method whatever we want,
    // as long as its signature matches the BrickEventHandler delegate.
    static void boat_BrickAdded(string message)
    {
      Console.WriteLine("Caught BrickAdded event! Message = " + message);
    }

    static void PromptForExit()
    {
      Console.Write(ln + "Program complete! Hit enter to exit...");
      Console.ReadLine();
    }
  }

  // The actual delegate is just a signature definition, and
  // it can be declared outside the class that it will be used in.
  // Remember that we can give the delegate as many parameters, and
  // as many different datatypes as we want! We could even create a
  // special BrickEventArgs class, which could contain information
  // like the total number of bricks, and then use that as the parameter!
  public delegate void BrickEventHandler(string message);

  public class Boat
  {
    private int _brickCount;

    // Declare an instance of the BrickEventHandler delegate.
    // The "event" keyword is optional, but it guarentees
    // that code outside of this class is only allowed to
    // add and remove methods from the delegate. Thus, Class1 cannot
    // directly assign to BrickAdded, even though it is public.
    public event BrickEventHandler BrickAdded;

    public Boat()
    {
      _brickCount = 0;

      // We have to initialize BrickAdded with at least one method,
      // so we create an OnBrickAdded method inside the class that
      // does nothing. Of course, we can put code in the OnBrickAdded
      // method if the boat object needs to do something everytime the
      // BrickAdded event occurs.
      BrickAdded = new BrickEventHandler(OnBrickAdded);
    }

    public void AddBrick()
    {
      ++_brickCount;
      string message = "Brick added.";

      // Just for fun, we'll make the message vary, depending
      // on how many bricks have been added.
      if( _brickCount == 3 )
        message += " The boat's getting heavier.";
      else if( _brickCount == 7 )
        message += " The boat can't take much more!";
      else if( _brickCount == 10 )
        message += " The boat sunk. Way to go.";

      // Here is where the BrickAdded event occurs. The code in Class1
      // calls the AddBrick() method, which in turn, fires a BrickAdded
      // event with a useful message that Class1 can catch. This allows
      // a boat object to communicate with its owner, without having
      // to know anything about who (or what) its owner actually is.
      BrickAdded(message);
    }

    // This is the "skeleton" method for BrickAdded. We haven't put
    // any code in here (although we could); we just need a method
    // that we can use to initialize BrickAdded.
    private void OnBrickAdded(string message)
    {
    }
  }
}