Posts:
Example    jeff
Using delegates in C#

Why use delegates?

Delegates make your code more extensible by allowing other developers to substitute their methods in place of your methods. Suppose you have a collection of business objects that has a LoadData method. You could create an overloaded version of the LoadData method that allows a developer to tell the LoadData method to call his/her custom method, via a delegate, to fetch the data for the collection.

What exactly is a delegate?

Delegates are probably one the areas of C# that confuse the most developers. Once you get a good understanding of how to create and use a delegate you will probably feel that it is really kind of simple.

The best way that I can describe a delegate is to make an analogy to classes and interfaces.

In object oriented programming we create classes that have methods and have implementation code in those methods. If you simply want to define the methods but not write the implementation code, you can create an interface that describes the methods but there is no code or functionality to go along with it.

A delegate is to a method as an interface is to a class. (Sounds like an SAT question!) In other words, if a function or method is considered equivalent to a class (because there is implementation code) then a delegate is equivalent to an interface. Basically, a delegate is a "function/method interface" in the sense that the delegate describes the return type and parameters of a function.

When, where, and how are delegates used?

For the sake of extensibility, there are times when you want to be able to describe a function's interface so that other develers can create functions/methods that are compatible and interchangeable.

The most common use of delegates in .NET and C# is when you define or subscribe to an event. Event declarations inform the developer, who is going to write the event handler method, what type of function is required to handle the event. When you define an event you are actually using a delegate as a middle-man so that the publisher of the event can call the subscriber's event handler method.

Another example is a "function pointer." There really is no such thing as a function pointer in C# because this functionality is also accomplished through the use of delegates. You create a method that can accept a delegate as a parameter, and when the method is invoked, the method that is associated with the delegate is executed.

Below is an example of how to use delegates as "function pointers" in C#.

Let's jump right in and define our delegate:

// *************************
// Delegates.cs
// *************************

using System;
using System.Data;

namespace DataAccess
{
    public delegate IDataReader DataFunctionDelegate(IDbConnection cn, int ID);
}

// *************************

Try to remember at this point that a delegate isn't really anything on its own, it simply describes the return type and parameters of a function/method. So what the DataFunctionDelegate is describing is a data function takes a connection parameter and an integer ID parameter and returns an IDataReader.

Now we can create a class that contains some static data functions. Note that the LoadByID data function matches the structure of the DataFunctionDelegate. The GetCustomerData method is here to load some test data, this method would not be part of a real-world application since the data would most likely come from a SQL data source.

// *************************
// CustomerData.cs
// *************************

using System;
using System.Data;
using System.Data.Common;

namespace DataAccess
{
    public class CustomerData
    {
        // **** NOTE: ****
        // normally the data functions would execute a stored procedure,
        // but I am using the GetCustomerData function to provide the
        // IDataReader to avoid the need to use SQL Server for this example

        public static IDataReader LoadAll(IDbConnection cn)
        {
            IDataReader dr = GetCustomerData(0);
            return dr;
        }

        public static IDataReader LoadByID(IDbConnection cn, int ID)
        {
            IDataReader dr = GetCustomerData(ID);
            return dr;
        }

        private static IDataReader GetCustomerData(int ID)
        {
            DataSet ds = new DataSet("CustomerDataSet");
            DataTable dt = new DataTable("Customer");
            DataColumn dc = null;

            // create the columns
            dc = new DataColumn("ID", typeof(int));
            dt.Columns.Add(dc);
            dc = new DataColumn("Name", typeof(string));
            dc.MaxLength = 150;
            dt.Columns.Add(dc);

            if (ID == 0)
            {
                // load all customers
                int i = 0;
                for (i = 1; i < 26; i++)
                {
                    dt.Rows.Add(i, "Customer " + i.ToString());
                }
            }
            else
            {
                // load the customer that matches ID
                dt.Rows.Add(ID, "Customer " + ID.ToString());
            }

            IDataReader dr = (DbDataReader)ds.CreateDataReader(dt);
            return dr;
        }
    }
}
// *************************

At the beginning of this article I mentioned a collection of business objects. Let's continue with that example... The collection has a LoadData method that is overloaded. Before we create the collection class, let's create the business object class for the objects that the collection will contain.

// *************************
// Customer.cs
// *************************

using System;
using System.Data;

namespace Business
{
    public class Customer
    {
        int _ID = 0;
        string _name = string.Empty;

        public Customer(int ID, string name)
        {
            this.ID = ID;
            this.Name = name;
        }

        public Customer(IDataReader dr)
        {
            this.ID = Convert.ToInt32(dr["ID"]);
            this.Name = Convert.ToString(dr["Name"]);
        }

        public int ID
        {
            get { return _ID; }
            set { _ID = value; }
        }

        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

    }
}

// *************************

Now let's create the business object collection class to hold the Customer objects. For the sake of simplicity, I have left out any code that is not relevant to delegates, so this collection will have limited functionality other than the LoadData methods.

// *************************
// Customer.cs
// *************************

using System;
using System.Collections;
using System.Data;
using DataAccess;

namespace Business
{
    public class CustomerCollection : CollectionBase
    {
        public void LoadData(IDataReader dr)
        {
            Customer obj = null;

            this.List.Clear();

            if (dr != null)
            {
                while (dr.Read())
                {
                    obj = new Customer(dr);
                    this.List.Add(obj);
                }
            }
        }

        public void LoadData(IDbConnection cn)
        {
            IDataReader dr = CustomerData.LoadAll(cn);

            this.LoadData(dr);

            if (dr != null)
            {
                if (!dr.IsClosed)
                {
                    dr.Close();
                }
                dr = null;
            }
        }

        public void LoadData(IDbConnection cn, int ID, DataFunctionDelegate delegateFunction)
        {
            IDataReader dr = delegateFunction(cn, ID);

            this.LoadData(dr);

            if (dr != null)
            {
                if (!dr.IsClosed)
                {
                    dr.Close();
                }
                dr = null;
            }
        }
    }
}

// *************************

The first LoadData method takes an IDataReader parameter. The IDataReader is a resultset that the LoadData method will iterate through creating and adding new objects for each record in the resultset. This LoadData method is important because all of the other LoadData methods, regardless of where they get their data, will call this LoadData method and pass in an IDataReader. (IDataReader is a good alternative to declaring your data access function return types and parameters as SqlDataReader since it will be much easier later on if you decide to support MySQL or some other data provider.)

The second LoadData method only takes an IDbConnection. This method simply loads all of the records in the table by calling a data function which in a real-world application would execute a stored procedure to return the results. In this application we just load some dummy data using the GetCustomerData function.

The third LoadData method takes an IDbConnection, an integer ID, and a DataFunctionDelegate delegate as parameters. This method is the one that we are really interested in. By creating this LoadData method, you are providing a way for other developers to tell your collection to call another data function when this LoadData method is executed.

Now it is time to see this code in action. Create a Web Form using the code below.

The Web Form ASPX file:

// *************************
// DelegateExample.aspx
// *************************

<%@ Page
Language="C#"
AutoEventWireup="true"
CodeFile="DelegateExample.aspx.cs"
Inherits="DelegateExample"
%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>C# Delegates - Function Pointers</title>
</head>
<body>
<form id="form1" runat="server">
<div style="text-align:center;">
        <asp:RadioButtonList runat="server" ID="rblLoad" AutoPostBack="true"
        RepeatDirection="horizontal" style="display:inline;">
        <asp:ListItem Text="All" Value="all" Selected="true" />
        <asp:ListItem Text="Odd" Value="odd" Selected="false" />
        <asp:ListItem Text="Even" Value="even" Selected="false" />
        <asp:ListItem Text="By ID" Value="id" Selected="false" />
        </asp:RadioButtonList>
        <asp:TextBox runat="server" id="txtID" Text="1" Columns="5" />
        <br />
        <asp:GridView runat="server" ID="gvCustomers" AutoGenerateColumns="true" />
</div>
</form>
</body>
</html>

// *************************

The Web Form Code File:

// *************************
// DelegateExample.aspx.cs
// *************************

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Data.Common;
using DataAccess;
using Business;

public partial class DelegateExample : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // this example doesn't really use the connection,
        // so we can leave it null. In a real application
        // you would want to establish a connection to a database
        IDbConnection cn = null;
        CustomerCollection customers = new CustomerCollection();

        int ID = 0;
        DataFunctionDelegate dfd = null;

        switch (this.rblLoad.SelectedValue)
        {
            case "id":
                // load a customer by ID
                ID = Convert.ToInt32(this.txtID.Text);
                dfd = new DataFunctionDelegate(CustomerData.LoadByID);
                customers.LoadData(cn, ID, dfd);
                break;

            case "odd":
                // load customers with odd IDs

                // create the DataFunctionDelegate and pass
                // in the method that you want to get executed
                dfd = new DataFunctionDelegate(LoadCustomersOdd);

                // now pass the DataFunctionDelegate to the
                // LoadData method as a "function pointer"
                customers.LoadData(cn, ID, dfd);
                break;

            case "even":
                // load customers with even IDs

                // create the DataFunctionDelegate and pass
                // in the method that you want to get executed
                dfd = new DataFunctionDelegate(LoadCustomersEven);

                // now pass the DataFunctionDelegate to the
                // LoadData method as a "function pointer"
                customers.LoadData(cn, ID, dfd);
                break;

            default:
            case "all":
                // load all customer records
                customers.LoadData(cn);
                break;
        }
        this.gvCustomers.DataSource = customers;
        this.gvCustomers.DataBind();
    }

    // some custom data functions

    // NOTE: we don't use the ID in the custom data functions but to
    // match the delegate structure we must have the same parameter list
    // as defined in the delegate declaration

    private static IDataReader LoadCustomersOdd(IDbConnection cn, int ID)
    {
        return LoadCustomerData(1); // modResult = 1 will return odd numbered Customer IDs
    }
    private static IDataReader LoadCustomersEven(IDbConnection cn, int ID)
    {
        return LoadCustomerData(0); // modResult = 0 will return even numbered Customer IDs
    }

    private static IDataReader LoadCustomerData(int modResult)
    {
        DataSet ds = new DataSet("CustomerDataSet");
        DataTable dt = new DataTable("Customer");
        DataColumn dc = null;

        // create the columns
        dc = new DataColumn("ID", typeof(int));
        dt.Columns.Add(dc);
        dc = new DataColumn("Name", typeof(string));
        dc.MaxLength = 150;
        dt.Columns.Add(dc);

        int i = 0;
        for (i = 1; i < 26; i++)
        {
            if (i % 2 == modResult)
            {
                dt.Rows.Add(i, "Customer " + i.ToString());
            }
        }
        
        IDataReader dr = (DbDataReader)ds.CreateDataReader(dt);
        return dr;
    }
}

// *************************

Since I am not using SQL Server in this example, it isn't quite as clear how convenient this code is. Try to imagine each of the data functions calling different stored procedures, or executing custom SQL statements. Additionally, imagine that the DataAccess namespace and the Business namespace are compiled into a DLL and you do not have access to the code. When you think about it this way, you may see how the delegate helps a developer (user of the DLL) extend the functionality despite the fact that he doesn't have the code and cannot recompile the DLL.

Questions and comments are welcome!

-Jeff