ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Writing C# Custom Events

by Budi Kurniawan
04/15/2002

Anyone doing Windows programming must have done some event handling in some way or another: capturing the double-click of a button, handling the click of a menu item, reacting to the moving of the mouse pointer over a label, and so forth. But what about creating your own event, in your own control, and letting others capture that event? In this article, you will learn how to use the observer design pattern to raise and handle events for your .NET control and learn how to pass event argument data.

An event is a message sent by an object to notify other objects that an action has occurred. The action could be user-initiated, such as a mouse click, or it could be triggered by some other program logic. The object that raises the event is called the event sender, and the object that receives the event notification is called the event receiver. The event receiver has a method that gets executed automatically in response to the event.

The .NET Framework supports easy event-driven Windows programming. It's so easy that often the programmer does not have to know how events work in the underlying .NET technology. All one has to remember is this: if you are interested in receiving an event from a Windows control, you provide an event handler and register the event handler with the event source. This is called event wiring. In C#, you need to write a line of code of the following syntax, normally in the class' constructor of your form.


eventSource.someEvent += new SomeEventHandler(someMethod);

For instance, if you want to handle the Click event of a button named button1, and you want the private button1_Click method to be executed when the Click event occurs, you write the following.


button1.Click += new EventHandler(button1_Clicked);

Then, you must also provide the implementation of button1_Click in your class, as follows.


private void button1_Clicked(Object sender, EventArgs e)
{
  // code to be executed when the Click event occurs
}

The method does not have to be private, but it must accept two arguments: an object of type Object and an EventArgs object. So consuming events is a piece of cake. Now, let's have a look at creating your own custom event that other programmers can use. Before we do this, however, we should first discuss the observer pattern in object-oriented programming.

The Observer Pattern

There are two key objects in this pattern: the subject and the observer. The subject may have one or many observers. These observers listen for notification from the subject of a state change inside the subject. This pattern is also known asdependence or publish-subscribe. According to The Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides) in their book Design Patterns: Elements of Reusable Object-Oriented Software, the Observer pattern can be applied in the following situations:

Related Reading

Programming C#
By Jesse Liberty

In event communication, the event sender class does not know which object or method will receive the events it raises. What is needed is an intermediary (or pointer-like mechanism) between the source and the receiver. The .NET Framework defines a special type, Delegate, that provides the functionality of a function pointer.

A delegate is a class that can hold a reference to a method. Unlike other classes, a delegate class has a signature, and it can hold references only to methods that match its signature. A delegate is thus equivalent to a type-safe function pointer, or a callback. The next section describes how to use delegates to communicate an event from an object to another object. The section after that puts the theory into practice by providing a control that has two custom events.

Writing a Custom Event -- Step-by-Step

Suppose you want to write a custom event called MyEvent for your custom control named MyControl that extends System.Windows.Forms.UserControl. Here are the steps you need to take.

1. Declare a delegate with the public access modifier. Here I will call this delegate MyEventHandler, and it has two arguments: an object called sender and MyEventArgs called e. We will define MyEventArgs later. Note that the delegate must be declared outside of your control class.


public delegate void MyEventHandler(object sender, MyEventArgs e);
public class MyControl: UserControl
{
   ...
}

2. MyEventArgs in Step 1 is the object that contains the data that can be passed from the event sender (MyControl) to the event receiver. MyEventArgs must extend the System.EventArgs class. Therefore, you now have the following code:


public class MyEventArgs: EventArgs
{
  ...
}

public delegate void MyEventHandler(object sender, MyEventArgs e);

public class MyControl: UserControl
{
   ...
}

There is some implementation you need to write inside the MyEventArgs class, but we will leave it until later.

3. In your control class, declare an event called MyEvent.


public class MyEventArgs: EventArgs
{
  ...
}

public delegate void MyEventHandler(object sender, MyEventArgs e);

public class MyControl: UserControl
{
  public event MyEventHandler MyEvent;
   ...
}

4. In your control class, declare a protected virtual method named On plus the name of the event. Since our event in this example is called MyEvent, the method is called OnMyEvent. Note that OnMyEvent has one argument of type MyEventArgs. Inside of this method, you raise the event. In C#, raising an event is achieved by calling the event name. To raise the event, you pass two arguments: the sender (the control MyControl) and the MyEventArgs object passed to the method.


public class MyEventArgs: EventArgs
{
  ...
}

public delegate void MyEventHandler(object sender, MyEventArgs e);

public class MyControl: UserControl
{
  public event MyEventHandler MyEvent;
  protected virtual void OnMyEvent(MyEventArgs e)
  {
    MyEvent(this, e)
  }
   ...
}

5. Now, the only step left is to actually call OnMyEvent from somewhere in the MyControl class. How you do this depends on what should cause the event to occur. This will become clear in the next section, when I present the real control that implements two events. Afterwards, users of your control can consume the MyEvent event in your control by wiring the event to an event handler in their form, as shown at the beginning of this article. Now, let's see the real code with two events and demonstrate how these events can be raised and consumed.

Putting Theory Into Practice

The following example uses a custom control called WhiteBoard. The WhiteBoard class inherits the System.Windows.Forms.UserControl. As the name implies, this control acts like a small white board (10 characters x 10 characters) into which the user can type. In addition, there is a caret that blinks and indicates the current insertion point. The user can use the arrow keys to move the caret, and hence, the insertion point. The user can also use control keys, such as Ctrl+R, Ctrl+G, and Ctrl+B to change the background color of the control, and Ctrl+Alt+R, Ctrl+Alt+G, and Ctrl+Alt+B to change the text color. In this article, we will modify the control to add two events:

We will then capture these two events in a form to dynamically update a label that shows the caret's current position. As an overview, the WhiteBoard control is briefly explained here:

Now, let's see how we can add two events to the WhiteBoard control: LineChanged and ColumnChanged. We do so by following the steps in the previous sections.

1. Declare two delegates: LineEventHandler and ColumnEventHandler.


  public delegate void LineEventHandler(Object sender, LineEventArgs e);
  public delegate void ColumnEventHandler(Object sender, ColumnEventArgs e);

2. Define and provide implementations for LineEventArgs and ColumnEventArgs.


public class LineEventArgs: EventArgs
  {
    private int oldValue, newValue;
    public LineEventArgs(int oldValue, int newValue)
    {
      this.oldValue = oldValue;
      this.newValue = newValue;
    }

    public int NewLine
    {
      get
      {
        return newValue;
      }
    }
    public int OldLine
    {
      get
      {
        return oldValue;
      }
    }
  }
  public class ColumnEventArgs: EventArgs
  {

    private int oldValue, newValue;
    public ColumnEventArgs(int oldValue, int newValue)
    {
      this.oldValue = oldValue;
      this.newValue = newValue;
    }

    public int NewColumn
    {
      get
      {
        return newValue;
      }
    }
    public int OldColumn
    {
      get
      {
        return oldValue;
      }
    }
  }

Both classes have a constructor that accepts two integers: the old value and the new value. Both classes also have read-only properties to obtain these integers. In LineEventArgs, the properties are OldLine and NewLine. In ColumnEventArgs, the properties are OldColumn and NewColumn.

3. Declare the LineChanged and ColumnChanged events in the control class.


public event LineEventHandler LineChanged;
public event ColumnEventHandler ColumnChanged;

4. In the WhiteBoard class, declare two protected virtual methods for the two events: OnLineChanged and OnColumnChanged. In OnLineChanged, you raise the LineChanged event. In OnColumnChanged, you raise the ColumnChanged event.


    protected virtual void OnLineChanged(LineEventArgs e)
    {
      LineChanged(this, e);
    }
    protected virtual void OnColumnChanged(ColumnEventArgs e)
    {
      ColumnChanged(this, e);
    }

5. Now, call the OnLineChanged and OnColumnChanged methods from inside of the WhiteBoard class. The LineChanged event is triggered when the y coordinate of the caret changes, i.e., when the CaretY property changes value. Similarly, the ColumnChanged event occurs when the CaretX property changes value. Since the caretY and caretX fields are only accessed through the CaretY and CaretX properties, we only raise events from these properties to guarantee that every change in CaretY raises the LineChanged event, and that every time the value of CaretX changes, the ColumnChanged event is triggered.


    public int CaretX
    {
      get
      {
        return caretX;
      }
      set
      {
        int oldValue = caretX;
        caretX = value;
        if (oldValue != caretX)
          OnColumnChanged(new ColumnEventArgs(oldValue, caretX));
      }
    }

    public int CaretY
    {
      get
      {
        return caretY;
      }
      set
      {
        int oldValue = caretY;
        caretY = value;
        if (oldValue != caretY)
          OnLineChanged(new LineEventArgs(oldValue, caretY));
      }
    }

The complete code of the WhiteBoard control, delegates, and event argument classes are given in Listing 1.

Consuming Events

After you compile the WhiteBoard control, you can now use it in a form. An example of such as form is shown in Figure 1. Note there is a label that displays the line and column position of the caret. The text of the label changes every time you move the caret. The code for the form is given in Listing 2.

Figure 1: Consuming the events in a form

Listing 2: Using the custom events in the WhiteBoard control


using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace CustomEvent
{
  public class Form1 : System.Windows.Forms.Form
    {
      private CustomEvent.WhiteBoard whiteBoard;
      private System.ComponentModel.Container components = null;
      private Label label;
      private int column, line;

      public Form1()
      {
        InitializeComponent();
      }

      protected override void Dispose( bool disposing )
      {
        if( disposing )
        {
          if (components != null) 
          {
            components.Dispose();
          }
        }
        base.Dispose( disposing );
      }

    private void whiteBoard_ColumnChanged(Object sender, ColumnEventArgs e)
    {
      column = e.NewColumn;
      label.Text = "Line:" + line.ToString() + "    Column:" + column.ToString();  
    }
    private void whiteBoard_LineChanged(Object sender, LineEventArgs e)
    {
      line = e.NewLine;
      label.Text = "Line:" + line.ToString() + "    Column:" + column.ToString();  
    }
#region Windows Form Designer generated code
      private void InitializeComponent()
      {
        whiteBoard= new WhiteBoard();
        this.SuspendLayout();
        whiteBoard.Location = new Point(20,20);
        whiteBoard.Size = new Size(190, 220);
        whiteBoard.ColumnChanged += new 
ColumnEventHandler(whiteBoard_ColumnChanged); 
        whiteBoard.LineChanged += new LineEventHandler(whiteBoard_LineChanged); 
        
        label = new Label();
        this.label.Location = new Point(20, 250);
        this.label.Size = new Size(100, 20);
        
        this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
        this.ClientSize = new System.Drawing.Size(292, 273);
        
        this.Controls.AddRange(new System.Windows.Forms.Control[] {whiteBoard, label});
        this.Name = "Form1";
        this.Text = "Small Whiteboard";
        this.ResumeLayout(false);

      }
#endregion

      [STAThread]
      static void Main() 
      {
        Application.Run(new Form1());
      }
    }
  }

Budi Kurniawan is a senior J2EE architect and author.


Return to the .NET DevCenter.

Copyright © 2009 O'Reilly Media, Inc.