Search

Loading...

Friday, 27 May 2011

Data Access using Dynamics - Part II: The DynamicDataSet


Introduction

In my previous article, I demonstrated how to leverage the Dynamic Language Runtime (DLR) to create a flexible, dynamic wrapper for the ADO.NET DbDataReader class and its subclasses.

In this article, we are going to look at creating a dynamic version of another staple component of the ADO.NET framework, namely the DataSet. Again, I'm sure many seasoned .NET developers will recognise the following hypothetical example:

// C#
using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["MyDatabase"].ConnectionString))
{
    string sql = "SELECT SomeColumn, AnotherColumn FROM SomeTable";
    SqlCommand command = new SqlCommand(sql, connection);
    SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
    DataSet dataSet = new DataSet();
    dataAdapter.Fill(dataSet, "SomeTable");
    foreach (DataRow row in dataSet.Tables["SomeTable"].Rows)
    {
        int foo = (int)row["SomeColumn"];
        string bar = (string)row["AnotherColumn"];
        // Do some stuff with the data.
    }
}
' Visual Basic
Using connection As SqlConnection = New SqlConnection(ConfigurationManager.ConnectionStrings("MyDatabase").ConnectionString)
    Dim sql As String = "SELECT SomeColumn, AnotherColumn FROM SomeTable"
    Dim command As SqlCommand = New SqlCommand(sql, connection)
    Dim dataAdapter As SqlDataAdapter = New SqlDataAdapter(command)
    Dim dataSet As DataSet = New DataSet()
    dataAdapter.Fill(dataSet, "SomeTable")
    For Each row As DataRow In dataSet.Tables("SomeTable").Rows
        Dim foo As Integer = DirectCast(row("SomeColumn"), Integer)
        Dim bar As String = DirectCast(row("AnotherColumn"), Integer)
        ' Do some stuff with the data.
    Next
End Using

The Dynamic DataSet and its Components

Before we look at the code for the DynamicDataSet class and all its component classes, we first need to extend the DynamicDataObjectWrapper class we looked at in the previous article:

// C#
public abstract class DynamicListSourceDataObjectWrapper<T> : DynamicDataObjectWrapper<T>, IListSource
    where T : IListSource
{
    public DynamicListSourceDataObjectWrapper(T obj)
        : base(obj)
    {
    }

    public virtual bool ContainsListCollection
    {
        get { return Obj.ContainsListCollection; }
    }

    public virtual IList GetList()
    {
        return Obj.GetList();
    }
}
' Visual Basic
Public MustInherit Class DynamicListSourceDataObjectWrapper(Of T As IListSource)
    Inherits DynamicDataObjectWrapper(Of T)
    Implements IListSource

    Public Sub New(ByVal obj As T)
        MyBase.New(obj)
    End Sub

    Public ReadOnly Property ContainsListCollection As Boolean Implements IListSource.ContainsListCollection
        Get
            Return Obj.ContainsListCollection
        End Get
    End Property

    Public Function GetList() As System.Collections.IList Implements IListSource.GetList
        Return Obj.GetList()
    End Function
End Class

As you can see, this class implements the IListSource, which will enable us to use our DynamicDataSet and DynamicDataTable classes with the standard ASP.NET data controls.

Now, let's look a the code for the DynamicDataSet class:

// C#
public class DynamicDataSet : DynamicListSourceDataObjectWrapper<DataSet>
{
    public DynamicDataSet()
        : this(new DataSet())
    {
    }

    public DynamicDataSet(DataSet dataSet)
        : base(dataSet)
    {
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (base.TryGetMember(binder, out result))
            return true;
        else
        {
            try
            {
                if (Obj.Tables.Contains(binder.Name))
                    result = (DynamicDataTable)Obj.Tables[binder.Name];
                else
                    result = (DynamicDataTable)Obj.Tables.Add(binder.Name);
                return true;
            }
            catch (Exception)
            {
                result = null;
                return false;
            }
        }
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (base.TrySetMember(binder, value))
            return true;
        else
        {
            try
            {
                dynamic table = value;
                table.TableName = binder.Name;
                Obj.Tables.Add(table);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
    {
        try
        {
            object index = indexes[0];
            if (index is int)
            {
                result = (DynamicDataTable)Obj.Tables[(int)index];
                return true;
            }
            else if (index is string)
            {
                result = (DynamicDataTable)Obj.Tables[(string)index];
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }
        catch (Exception)
        {
            result = null;
            return false;
        }
    }

    public static implicit operator DataSet(DynamicDataSet dataSet)
    {
        return dataSet.Obj;
    }

    public static explicit operator DynamicDataSet(DataSet dataSet)
    {
        return new DynamicDataSet(dataSet);
    }
}
' Visual Basic
Public Class DynamicDataSet
    Inherits DynamicListSourceDataObjectWrapper(Of DataSet)

    Public Sub New()
        Me.New(New DataSet())
    End Sub

    Public Sub New(ByVal dataSet As DataSet)
        MyBase.New(dataSet)
    End Sub

    Public Overrides Function TryGetMember(binder As GetMemberBinder, ByRef result As Object) As Boolean
        If MyBase.TryGetMember(binder, result) Then
            Return True
        Else
            Try
                If Obj.Tables.Contains(binder.Name) Then
                    result = CType(Obj.Tables(binder.Name), DynamicDataTable)
                Else
                    result = CType(Obj.Tables.Add(binder.Name), DynamicDataTable)
                End If
                Return True
            Catch ex As Exception
                result = Nothing
                Return False
            End Try
        End If
    End Function

    Public Overrides Function TrySetMember(binder As SetMemberBinder, value As Object) As Boolean
        If MyBase.TrySetMember(binder, value) Then
            Return True
        Else
            Try
                Dim table As Object = value
                table.TableName = binder.Name
                Obj.Tables.Add(table)
                Return True
            Catch ex As Exception
                Return False
            End Try
        End If
    End Function

    Public Overrides Function TryGetIndex(binder As GetIndexBinder, indexes() As Object, ByRef result As Object) As Boolean
        If MyBase.TryGetIndex(binder, indexes, result) Then
            Return True
        Else
            Try
                Dim index As Object = indexes(0)
                If TypeOf (index) Is Integer Then
                    result = CType(Obj.Tables(DirectCast(index, Integer)), DynamicDataTable)
                    Return True
                ElseIf TypeOf (index) Is String Then
                    result = CType(Obj.Tables(DirectCast(index, String)), DynamicDataTable)
                    Return True
                Else
                    result = Nothing
                    Return False
                End If
            Catch ex As Exception
                result = Nothing
                Return False
            End Try
        End If
    End Function

    Public Shared Widening Operator CType(ByVal dataSet As DynamicDataSet) As DataSet
        Return dataSet.Obj
    End Operator

    Public Shared Narrowing Operator CType(ByVal dataSet As DataSet) As DynamicDataSet
        Return New DynamicDataSet(dataSet)
    End Operator

End Class

Notice how we provide overrides for both the TryGetMember() and TrySetMember() methods which will allow us not only to select a table from our data set via dynamic properties, but also to create a new table. The TryGetIndex() and TrySetIndex() overrides allow us to do the same thing using indexers.

As with the DynamicDataReader class from the previous article, we also provide a pair of conversion operators for easy conversion between the static object and its dynamic wrapper. This is a pattern we will follow throughout this exercise.

Now it is time to take a look at the DynamicDataTable class. Like the DynamicDataSet, it inherits from DynamicListSourceDataObjectWrapper, allowing it to be used with ASP.NET data controls. We override the TryGetMember() and TrySetMember() methods in such a way that accessing a dynamic property will return the appropriate column from the table; and setting a dynamic property will create a new column in the table:

// C#
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    if (base.TryGetMember(binder, out result))
        return true;
    else
    {
        try
        {
            result = Obj.Columns[binder.Name];
            return true;
        }
        catch
        {
            result = null;
            return false;
        }
    }
}

public override bool TrySetMember(SetMemberBinder binder, object value)
{
    if (base.TrySetMember(binder, value))
        return true;
    else
    {
        try
        {
            Type columnType = (Type)value;
            Obj.Columns.Add(binder.Name, columnType);
            return true;
        }
        catch(Exception)
        {
            return false;
        }
    }
}
' Visual Basic
Public Overrides Function TryGetMember(binder As GetMemberBinder, ByRef result As Object) As Boolean
    If MyBase.TryGetMember(binder, result) Then
        Return True
    Else
        Try
            result = Obj.Columns(binder.Name)
            Return True
        Catch ex As Exception
            result = Nothing
            Return False
        End Try
    End If
End Function

Public Overrides Function TrySetMember(binder As SetMemberBinder, value As Object) As Boolean
    If MyBase.TrySetMember(binder, value) Then
        Return True
    Else
        Try
            Dim columnType As Type = DirectCast(value, Type)
            Obj.Columns.Add(binder.Name, columnType)
            Return True
        Catch ex As Exception
            Return False
        End Try
    End If
End Function

With the TryGetIndex(), however, we are going to be slightly more creative. If the indexer is accessed via an integer, then we return the respective row from the table. On the other hand, if the indexer is accessed via a string, then we return the respective column from the table:

// C#
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
    try
    {
        object index = indexes[0];
        if (index is int)
        {
            result = (DynamicDataRow)Obj.Rows[(int)index];
            return true;
        }
        else if (index is string)
        {
            result = Obj.Columns[(string)index];
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }
    catch (Exception)
    {
        result = null;
        return false;
    }
}
' Visual Basic
Public Overrides Function TryGetIndex(binder As GetIndexBinder, indexes() As Object, ByRef result As Object) As Boolean
    If MyBase.TryGetIndex(binder, indexes, result) Then
        Return True
    Else
        Try
            Dim index As Object = indexes(0)
            If TypeOf (index) Is Integer Then
                result = CType(Obj.Rows(DirectCast(index, Integer)), DynamicDataRow)
                Return True
            ElseIf TypeOf (index) Is String Then
                result = Obj.Columns(DirectCast(index, String))
                Return True
            Else
                result = Nothing
                Return False
            End If
        Catch ex As Exception
            result = Nothing
            Return False
        End Try
    End If
End Function

The DynamicDataTable also implements the IEnumerable interface so that we can easily iterate through all the rows in the table without having to explicitly call the Rows property:

// C#
public IEnumerator GetEnumerator()
{
    return new DynamicDataTableEnumerator(Obj.Rows.GetEnumerator());
}

private class DynamicDataTableEnumerator : IEnumerator
{
    private IEnumerator enumerator;

    public DynamicDataTableEnumerator(IEnumerator enumerator)
    {
        this.enumerator = enumerator;
    }

    public object Current
    {
        get { return (DynamicDataRow)(DataRow)enumerator.Current; }
    }

    public bool MoveNext()
    {
        return enumerator.MoveNext();
    }

    public void Reset()
    {
        enumerator.Reset();
    }
}
' Visual Basic
Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
    Return New DynamicDataTableEnumerator(Obj.Rows.GetEnumerator())
End Function

Private Class DynamicDataTableEnumerator
    Implements IEnumerator

    Private _enumerator As IEnumerator

    Public Sub New(ByVal enumerator As IEnumerator)
        _enumerator = enumerator
    End Sub

    Public ReadOnly Property Current As Object Implements IEnumerator.Current
        Get
            Return CType(DirectCast(_enumerator.Current, DataRow), DynamicDataRow)
        End Get
    End Property

    Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
        Return _enumerator.MoveNext()
    End Function

    Public Sub Reset() Implements IEnumerator.Reset
        _enumerator.Reset()
    End Sub
End Class

This finally leaves the DynamicDataRow class. Here we override the TryGetMember() and TrySetMember() methods to get and set the appropriate column value respectively, using the indexers of the wrapped DataRow object. We also override the TryGetIndex() and TrySetIndex() methods so we still have the option of using the indexers if we wish. For the sake of brevity, I've omitted the code, as it is very similar to what we've seen already.

Now for some Examples

Here are some examples of our DynamicDataSet in action. As in the previous article, they all use the Northwind database are not intended to serve as examples of good data-access practice. Firstly, selecting from a database table and populating a data object:

// C#
public static Employee[] GetEmployees()
{
    List<Employee> employees = new List<Employee>();
    using (SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString))
    {
        string query = "SELECT EmployeeID, LastName, FirstName, Title, TitleOfCourtesy, BirthDate, HireDate, Address, City, Region, PostalCode, Country, HomePhone, Extension, Photo, Notes, ReportsTo, PhotoPath " +
                       "FROM dbo.Employees";
        SqlCommand command = new SqlCommand(query, connection);
        SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
        dynamic dataSet = new DynamicDataSet();
        dataAdapter.Fill(dataSet, "Staff");
        foreach (dynamic row in dataSet.Staff)
        {
            Employee employee = new Employee()
            {
                Id = row.EmployeeID,
                Surname = row.LastName,
                FirstName = row.FirstName,
                Title = row.Title,
                CourtesyTitle = row.TitleOfCourtesy,
                DateOfBirth = row.BirthDate,
                DateHired = row.HireDate,
                Address = row.Address,
                City = row.City,
                Region = row.Region,
                PostCode = row.PostalCode,
                Country = row.Country,
                HomePhone = row.HomePhone,
                Extension = row.Extension,
                Photo = row.Photo,
                Notes = row.Notes,
            };
            employees.Add(employee);
        }
    }
    return employees.ToArray();
}
' Visual Basic
Public Shared Function GetEmployees() As Employee()
    Dim employees As List(Of Employee) = New List(Of Employee)
    Using connection As SqlConnection = New SqlConnection(WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString)
        Dim query As String = "SELECT EmployeeID, LastName, FirstName, Title, TitleOfCourtesy, BirthDate, HireDate, Address, City, Region, PostalCode, Country, HomePhone, Extension, Photo, Notes, ReportsTo, PhotoPath " + _
                              "FROM dbo.Employees"
        Dim command As SqlCommand = New SqlCommand(query, connection)
        Dim dataAdapter As SqlDataAdapter = New SqlDataAdapter(command)
        Dim dataSet As DynamicDataSet = New DynamicDataSet
        dataAdapter.Fill(dataSet, "Staff")
        For Each row As Object In DirectCast(dataSet, Object).Staff
            Dim employee As Employee = New Employee() With _
                                       { _
                                           .Id = row.EmployeeID, _
                                           .Surname = row.LastName, _
                                           .FirstName = row.FirstName, _
                                           .Title = row.Title, _
                                           .CourtesyTitle = row.TitleOfCourtesy, _
                                           .DateOfBirth = row.BirthDate, _
                                           .DateHired = row.HireDate, _
                                           .Address = row.Address, _
                                           .City = row.City, _
                                           .Region = row.Region, _
                                           .PostCode = row.PostalCode, _
                                           .Country = row.Country, _
                                           .HomePhone = row.HomePhone, _
                                           .Extension = row.Extension, _
                                           .Photo = row.Photo, _
                                           .Notes = row.Notes _
                                       }
            employees.Add(employee)
        Next
    End Using
    Return employees.ToArray()
End Function

The following example shows how to create a new table in a data set and use it to insert the its values into a table in the database:


<form id="form1" runat="server">
<div>
    <h2>
        Insert Example</h2>
    <div>
        <table>
            <tbody>
                <tr>
                    <th>
                        First Name:
                    </th>
                    <td>
                        <asp:TextBox ID="firstNameTextBox" runat="server" />
                    </td>
                </tr>
                <tr>
                    <th>
                        Surname:
                    </th>
                    <td>
                        <asp:TextBox ID="surnameTextBox" runat="server" />
                    </td>
                </tr>
                <tr>
                    <th>
                        Home Phone:
                    </th>
                    <td>
                        <asp:TextBox ID="homePhoneTextBox" runat="server" />
                    </td>
                </tr>
                <tr>
                    <th>
                        Extension:
                    </th>
                    <td>
                        <asp:TextBox ID="extensionTextBox" runat="server" />
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
    <div>
        <asp:Button ID="createButton" runat="server" Text="Create" OnClick="createButton_Click" />
        <asp:Label ID="resultLabel" runat="server" ForeColor="Red" />
    </div>
</div>
</form>
// C#
protected void createButton_Click(object sender, EventArgs e)
{
    using (SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString))
    {
        string insertQuery = "INSERT INTO dbo.Employees(FirstName, LastName, HomePhone, Extension) " +
                             "VALUES(@FirstName, @LastName, @HomePhone, @Extension)";
        SqlCommand insertCommand = new SqlCommand(insertQuery, connection);
        insertCommand.Parameters.Add("@FirstName", SqlDbType.NVarChar, 10).SourceColumn = "FirstName";
        insertCommand.Parameters.Add("@LastName", SqlDbType.NVarChar, 20).SourceColumn = "LastName";
        insertCommand.Parameters.Add("@HomePhone", SqlDbType.NVarChar, 24).SourceColumn = "HomePhone";
        insertCommand.Parameters.Add("@Extension", SqlDbType.NVarChar, 4).SourceColumn = "Extension";
        SqlDataAdapter dataAdapter = new SqlDataAdapter() { InsertCommand = insertCommand };
        dynamic dataSet = new DynamicDataSet();
        dataSet.Employees.EmployeeID = typeof(int);
        dataSet.Employees.FirstName = typeof(string);
        dataSet.Employees.LastName = typeof(string);
        dataSet.Employees.HomePhone = typeof(string);
        dataSet.Employees.Extension = typeof(string);
        dynamic newRow = dataSet.Employees.NewRow();
        newRow.FirstName = firstNameTextBox.Text;
        newRow.LastName = surnameTextBox.Text;
        newRow.HomePhone = homePhoneTextBox.Text;
        newRow.Extension = extensionTextBox.Text;
        dataSet.Employees.Rows.Add(newRow);
        dataAdapter.Update(dataSet.Employees);
    }
    resultLabel.Text = "Item created!";
}
' Visual Basic
Protected Sub createButton_Click(ByVal sender As Object, ByVal e As EventArgs) Handles createButton.Click
    Using connection As SqlConnection = New SqlConnection(WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString)
        Dim insertQuery As String = "INSERT INTO dbo.Employees(FirstName, LastName, HomePhone, Extension) " + _
                                    "VALUES(@FirstName, @LastName, @HomePhone, @Extension)"
        Dim insertCommand As SqlCommand = New SqlCommand(insertQuery, connection)
        insertCommand.Parameters.Add("@FirstName", SqlDbType.NVarChar, 10).SourceColumn = "FirstName"
        insertCommand.Parameters.Add("@LastName", SqlDbType.NVarChar, 20).SourceColumn = "LastName"
        insertCommand.Parameters.Add("@HomePhone", SqlDbType.NVarChar, 24).SourceColumn = "HomePhone"
        insertCommand.Parameters.Add("@Extension", SqlDbType.NVarChar, 4).SourceColumn = "Extension"
        Dim dataAdapter As SqlDataAdapter = New SqlDataAdapter() With {.InsertCommand = insertCommand}
        Dim dataSet As Object = New DynamicDataSet()
        dataSet.Employees.EmployeeID = GetType(Integer)
        dataSet.Employees.FirstName = GetType(String)
        dataSet.Employees.LastName = GetType(String)
        dataSet.Employees.HomePhone = GetType(String)
        dataSet.Employees.Extension = GetType(String)
        Dim newRow As Object = dataSet.Employees.NewRow()
        newRow.FirstName = firstNameTextBox.Text
        newRow.LastName = surnameTextBox.Text
        newRow.HomePhone = homePhoneTextBox.Text
        newRow.Extension = extensionTextBox.Text
        dataSet.Employees.Rows.Add(newRow)
        dataAdapter.Update(dataSet.Employees)
    End Using
    resultLabel.Text = "Item created!"
End Sub

The following example shows how to use the DynamicDataSet to update a database table:


<form id="form1" runat="server">
<div>
    <h2>
        Update Example</h2>
    <div>
        <asp:DropDownList ID="employeeDropDown" runat="server" AppendDataBoundItems="True"
            DataTextField="LastName" DataValueField="EmployeeId" AutoPostBack="true" OnSelectedIndexChanged="employeeDropDown_SelectedIndexChanged">
            <asp:ListItem Text="Please select..." />
        </asp:DropDownList>
    </div>
    <div>
        <table>
            <tbody>
                <tr>
                    <th>
                        Home Phone:
                    </th>
                    <td>
                        <asp:TextBox ID="homePhoneTextBox" runat="server" />
                    </td>
                </tr>
                <tr>
                    <th>
                        Extension:
                    </th>
                    <td>
                        <asp:TextBox ID="extensionTextBox" runat="server" />
                    </td>
                </tr>
            </tbody>
        </table>
        <asp:Button ID="submitButton" runat="server" Text="Change" OnClick="submitButton_Click" />
    </div>
    <asp:Label ID="resultLabel" runat="server" ForeColor="Red" />
</div>
</form>
// C#
protected void submitButton_Click(object sender, EventArgs e)
{
    int employeeId = int.Parse(employeeDropDown.SelectedValue);
    string homePhone = homePhoneTextBox.Text;
    string extension = extensionTextBox.Text;
    ChangeContactNumbers(employeeId, homePhone, extension);
    resultLabel.Text = "Contact Details Changed";
}

private void ChangeContactNumbers(int employeeId, string homePhone, string extension)
{
    using (SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString))
    {
        string selectQuery = "SELECT EmployeeID, HomePhone, Extension " +
                             "FROM dbo.Employees " +
                             "WHERE EmployeeID = @EmployeeID";
        string updateQuery = "UPDATE dbo.Employees " +
                             "SET HomePhone = @HomePhone, " +
                             "Extension = @Extension " +
                             "WHERE EmployeeID = @EmployeeID";
        SqlCommand selectCommand = new SqlCommand(selectQuery, connection);
        selectCommand.Parameters.Add("@EmployeeID", SqlDbType.Int).Value = employeeId;
        SqlCommand updateCommand = new SqlCommand(updateQuery, connection);
        updateCommand.Parameters.Add("@EmployeeID", SqlDbType.Int).SourceColumn = "EmployeeID";
        updateCommand.Parameters.Add("@HomePhone", SqlDbType.NVarChar, 24).SourceColumn = "HomePhone";
        updateCommand.Parameters.Add("@Extension", SqlDbType.NVarChar, 4).SourceColumn = "Extension";
        SqlDataAdapter dataAdapter = new SqlDataAdapter() { SelectCommand = selectCommand, UpdateCommand = updateCommand };
        dynamic dataSet = new DynamicDataSet();
        dataAdapter.Fill(dataSet, "Employees");
        dataSet.Employees[0].HomePhone = homePhone;
        dataSet.Employees[0].Extension = extension;
        dataAdapter.Update(dataSet.Employees);
    }
}
' Visual Basic
Protected Sub submitButton_Click(ByVal sender As Object, ByVal e As EventArgs) Handles submitButton.Click
    Dim employeeId As Integer = Integer.Parse(employeeDropDown.SelectedValue)
    Dim homePhone As String = homePhoneTextBox.Text
    Dim extension As String = extensionTextBox.Text
    ChangeContactNumbers(employeeId, homePhone, extension)
    resultLabel.Text = "Contact Details Changed"
End Sub

Private Sub ChangeContactNumbers(ByVal employeeId As Integer, homePhone As String, ByVal extension As String)
    Using connection As SqlConnection = New SqlConnection(WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString)
        Dim selectQuery As String = "SELECT EmployeeID, HomePhone, Extension " + _
                                    "FROM dbo.Employees " + _
                                    "WHERE EmployeeID = @EmployeeID"
        Dim updateQuery As String = "UPDATE dbo.Employees " + _
                                    "SET HomePhone = @HomePhone, " + _
                                    "Extension = @Extension " + _
                                    "WHERE EmployeeID = @EmployeeID"
        Dim selectCommand As SqlCommand = New SqlCommand(selectQuery, connection)
        selectCommand.Parameters.Add("@EmployeeID", SqlDbType.Int).Value = employeeId
        Dim updateCommand As SqlCommand = New SqlCommand(updateQuery, connection)
        updateCommand.Parameters.Add("@EmployeeID", SqlDbType.Int).SourceColumn = "EmployeeID"
        updateCommand.Parameters.Add("@HomePhone", SqlDbType.NVarChar, 24).SourceColumn = "HomePhone"
        updateCommand.Parameters.Add("@Extension", SqlDbType.NVarChar, 4).SourceColumn = "Extension"
        Dim dataAdapter As SqlDataAdapter = New SqlDataAdapter() With {.SelectCommand = selectCommand, .UpdateCommand = updateCommand}
        Dim dataSet As Object = New DynamicDataSet()
        dataAdapter.Fill(DirectCast(dataSet, DynamicDataSet), "Employees")
        dataSet.Employees(0).HomePhone = homePhone
        dataSet.Employees(0).Extension = extension
        dataAdapter.Update(dataSet.Employees)
    End Using
End Sub

Finally, here is an example of how to delete a row from our DynamicDataTable:


<form id="form1" runat="server">
<div>
    <h2>
        Delete Example</h2>
    <div>
        <asp:DropDownList ID="employeeDropDown" runat="server" AppendDataBoundItems="True"
            DataTextField="LastName" DataValueField="EmployeeId">
        </asp:DropDownList>
        &nbsp;<asp:Button ID="deleteButton" runat="server" OnClick="deleteButton_Click" Text="Delete" />
    </div>
    <div>
        <asp:Label ID="resultLabel" runat="server" ForeColor="Red" />
    </div>
</div>
</form>
// C#
protected void deleteButton_Click(object sender, EventArgs e)
{
    using (SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString))
    {
        string selectQuery = "SELECT EmployeeID, LastName " +
                             "FROM dbo.Employees " +
                             "WHERE EmployeeID = @EmployeeID";
        string deleteQuery = "DELETE FROM dbo.Employees " +
                             "WHERE EmployeeID = @EmployeeID";
        SqlCommand selectCommand = new SqlCommand(selectQuery, connection);
        selectCommand.Parameters.Add("@EmployeeID", SqlDbType.Int).Value = int.Parse(employeeDropDown.SelectedValue);
        SqlCommand deleteCommand = new SqlCommand(deleteQuery, connection);
        deleteCommand.Parameters.Add("@EmployeeID", SqlDbType.Int).SourceColumn = "EmployeeID";
        SqlDataAdapter dataAdapter = new SqlDataAdapter() { SelectCommand = selectCommand, DeleteCommand = deleteCommand };
        dynamic dataSet = new DynamicDataSet();
        dataAdapter.Fill(dataSet, "Employees");
        dataSet.Employees[0].Delete();
        dataAdapter.Update(dataSet.Employees);
    }
    resultLabel.Text = "Item deleted.";
}
' Visual Basic
Protected Sub deleteButton_Click(ByVal sender As Object, ByVal e As EventArgs) Handles deleteButton.Click
    Using connection As SqlConnection = New SqlConnection(WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString)
        Dim selectQuery As String = "SELECT EmployeeID, LastName " + _
                                    "FROM dbo.Employees " + _
                                    "WHERE EmployeeID = @EmployeeID"
        Dim deleteQuery As String = "DELETE FROM dbo.Employees " + _
                                    "WHERE EmployeeID = @EmployeeID"
        Dim selectCommand As SqlCommand = New SqlCommand(selectQuery, connection)
        selectCommand.Parameters.Add("@EmployeeID", SqlDbType.Int).Value = Integer.Parse(employeeDropDown.SelectedValue)
        Dim deleteCommand As SqlCommand = New SqlCommand(deleteQuery, connection)
        deleteCommand.Parameters.Add("@EmployeeID", SqlDbType.Int).SourceColumn = "EmployeeID"
        Dim dataAdapter As SqlDataAdapter = New SqlDataAdapter() With {.SelectCommand = selectCommand, .DeleteCommand = deleteCommand}
        Dim dataSet As Object = New DynamicDataSet()
        dataAdapter.Fill(DirectCast(dataSet, DynamicDataSet), "Employees")
        dataSet.Employees(0).Delete()
        dataAdapter.Update(dataSet.Employees)
    End Using
    resultLabel.Text = "Item deleted."
End Sub

Summary

The DynamicDataSet and DynamicDataTable classes provide a loosely-typed means of data access which improves code readability and can be used with syntax which is an approximation to that found with strongly-typed datasets, but without the need to auto-generate the code beforehand.

The source code for this and the previous article, can be found here.

Tuesday, 24 May 2011

Data Access using Dynamics - Part I: The DynamicDataReader


A Trip Down Memory Lane

Way back in the mists of time, before the rise ORMs such as Entity Framework, LINQ-to-SQL or NHibernate; data access was typically performed using the ADO.NET DataReader and DataSet classes. To most .NET developers, I am sure the following scenario will be familiar:

// C#
using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["MyDatabase"].ConnectionString))
{
    string sql = "SELECT SomeColumn, AnotherColumn FROM SomeTable";
    SqlCommand command = new SqlCommand(sql, connection);
    connection.Open();
    IDataReader reader = command.ExecuteReader();
    while (reader.Read())
    {
        int foo = reader.GetInt32(reader.GetOrdinal("SomeColumn"));
        string bar = reader.GetString(reader.GetOrdinal("AnotherColumn"));
        // Do some stuff with the data.
    }
}
' Visual Basic
Using connection As SqlConnection = New SqlConnection(ConfigurationManager.ConnectionStrings("MyDatabase").ConnectionString)
    Dim sql As String = "SELECT SomeColumn, AnotherColumn FROM SomeTable"
    Dim command As SqlCommand = New SqlCommand(sql, connection)
    connection.Open()
    Dim reader As IDataReader = command.ExecuteReader()
    While reader.Read()
        Dim foo As Integer = reader.GetInt32(reader.GetOrdinal("SomeColumn"))
        Dim bar As String = reader.GetString(reader.GetOrdinal("AnotherColumn"))
        ' Do some stuff with the data.
    End While
End Using

Now recently, whilst having to work with directly with the ADO.NET classes for the first time in a very long time, I decided to experiment with trying to leverage the Dynamic Language Runtime (DLR) to create a set of loosely-typed data-access classes which can be used in a more object-oriented manner and hopefully aid code readability to boot.

The Dynamic DataReader

In this article, we are going to look at the DynamicDataReader class, which acts as a dynamic wrapper around the System.Data.Common.DbDataReader class. However, before we start looking at the code for this class in great detail, let's take a look at one of its parent classes: the DynamicDataWrapper class:

// C#
public abstract class DynamicDataObjectWrapper<T> : DynamicObject
{
    protected T Obj { get; private set; }
    protected Type ObjType { get; private set; }

    public DynamicDataObjectWrapper(T obj)
    {
        this.Obj = obj;
        this.ObjType = obj.GetType();
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        try
        {
            result = ObjType.InvokeMember(binder.Name, BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public, null, Obj, args);
            return true;

        }
        catch (Exception)
        {
            result = null;
            return false;
        }
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        PropertyInfo property = ObjType.GetProperty(binder.Name, BindingFlags.Instance | BindingFlags.Public);
        if (property != null)
        {
            result = property.GetValue(Obj, null);
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        PropertyInfo property = ObjType.GetProperty(binder.Name, BindingFlags.Instance | BindingFlags.Public);
        if (property != null)
        {
            property.SetValue(Obj, value, null);
            return true;
        }
        else
            return false;
    }
}
' Visual Basic
Public MustInherit Class DynamicDataObjectWrapper(Of T)
    Inherits DynamicObject

    Private _obj As T
    Private _objType As Type

    Public Sub New(ByVal obj As T)
        _obj = obj
        _objType = obj.GetType()
    End Sub

    Protected ReadOnly Property Obj As T
        Get
            Return _obj
        End Get
    End Property

    Protected ReadOnly Property ObjType As Type
        Get
            Return _objType
        End Get
    End Property

    Public Overrides Function TryInvokeMember(binder As System.Dynamic.InvokeMemberBinder, args() As Object, ByRef result As Object) As Boolean
        Try
            result = ObjType.InvokeMember(binder.Name, BindingFlags.InvokeMethod Or BindingFlags.Instance Or BindingFlags.Public, Nothing, Obj, args)
            Return True
        Catch ex As Exception
            result = Nothing
            Return False
        End Try
    End Function

    Public Overrides Function TryGetMember(binder As System.Dynamic.GetMemberBinder, ByRef result As Object) As Boolean
        Dim _property As PropertyInfo = ObjType.GetProperty(binder.Name, BindingFlags.Instance Or BindingFlags.Public)
        If Not _property Is Nothing Then
            result = _property.GetValue(Obj, Nothing)
            Return True
        Else
            result = Nothing
            Return False
        End If
    End Function

    Public Overrides Function TrySetMember(binder As System.Dynamic.SetMemberBinder, value As Object) As Boolean
        Dim _propety As PropertyInfo = ObjType.GetProperty(binder.Name, BindingFlags.Instance Or BindingFlags.Public)
        If Not _propety Is Nothing Then
            _propety.SetValue(Obj, value, Nothing)
            Return True
        Else
            Return False
        End If
    End Function

End Class

This class acts as dynamic wrapper for any non-dynamic object and, as such, inherits from DynamicObject. The wrapper overrides the TryGetMember(), TrySetMember() and TryInvokeMember methods such that any calls made on the dynamic object are passed to the underlying wrapped object. For example, if a call is made to a method called Foo() on the dynamic object, the Foo() method (if it exists) is called on the wrapped object.

Extending this is the DynamicEnumerableDataObjectWrapper, which is a subclass of DynamicDataObjectWrapper which implements the IEnumerable interface by passing calls to the GetEnumerator() method of the wrapped object:

// C#
public abstract class DynamicEnumerableDataObjectWrapper<T> : DynamicDataObjectWrapper<T>, IEnumerable
    where T : IEnumerable
{
    public DynamicEnumerableDataObjectWrapper(T obj)
        : base(obj)
    {
    }

    public virtual IEnumerator GetEnumerator()
    {
        return Obj.GetEnumerator();
    }
}
' Visual Basic
Public MustInherit Class DynamicEnumerableDataObjectWrapper(Of T As IEnumerable)
    Inherits DynamicDataObjectWrapper(Of T)
    Implements IEnumerable

    Public Sub New(ByVal obj As T)
        MyBase.New(obj)
    End Sub

    Public Overridable Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
        Return Obj.GetEnumerator()
    End Function
End Class

The DynamicDataReader class is, in turn, a concrete implementation of DynamicEnumerableDataObjectWrapper which wraps a DbDataReader object:

// C#
public class DynamicDataReader : DynamicEnumerableDataObjectWrapper<DbDataReader>
{
    public DynamicDataReader(DbDataReader reader)
        : base(reader)
    {
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (base.TryGetMember(binder, out result))
            return true;
        else
        {
            try
            {
                if (!Obj.IsDBNull(Obj.GetOrdinal(binder.Name)))
                    result = Obj.GetValue(Obj.GetOrdinal(binder.Name));
                else
                    result = null;
                return true;
            }
            catch (Exception)
            {
                result = null;
                return false;
            }
        }
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
    {
        try
        {
            object index = indexes[0];
            if (index is int)
            {
                int intIndex = (int)index;
                if (!Obj.IsDBNull(intIndex))
                    result = Obj.GetValue(intIndex);
                else
                    result = null;
                return true;
            }
            else if (index is string)
            {
                string strIndex = (string)index;
                if (!Obj.IsDBNull(Obj.GetOrdinal(strIndex)))
                    result = Obj.GetValue(Obj.GetOrdinal(strIndex));
                else
                    result = null;
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }
        catch(Exception)
        {
            result = null;
            return false;
        }
    }

    public static implicit operator DbDataReader(DynamicDataReader reader)
    {
        return reader.Obj;
    }

    public static explicit operator DynamicDataReader(DbDataReader reader)
    {
        return new DynamicDataReader(reader);
    }
}
' Visual Basic
Public Class DynamicDataReader
    Inherits DynamicEnumerableDataObjectWrapper(Of DbDataReader)

    Public Sub New(ByVal reader As DbDataReader)
        MyBase.New(reader)
    End Sub

    Public Overrides Function TryGetMember(binder As GetMemberBinder, ByRef result As Object) As Boolean
        If MyBase.TryGetMember(binder, result) Then
            Return True
        Else
            Try
                If Not Obj.IsDBNull(Obj.GetOrdinal(binder.Name)) Then
                    result = Obj.GetValue(Obj.GetOrdinal(binder.Name))
                Else
                    result = Nothing
                End If
                Return True
            Catch ex As Exception
                result = Nothing
                Return False
            End Try
        End If
    End Function

    Public Overrides Function TryGetIndex(binder As GetIndexBinder, indexes() As Object, ByRef result As Object) As Boolean
        Try
            Dim index As Object = indexes(0)
            If TypeOf (index) Is Integer Then
                Dim intIndex As Integer = DirectCast(index, Integer)
                If Not Obj.IsDBNull(intIndex) Then
                    result = Obj.GetValue(intIndex)
                Else
                    result = Nothing
                End If
                Return True
            ElseIf TypeOf (index) Is String Then
                Dim strIndex As String = DirectCast(index, String)
                If Not Obj.IsDBNull(Obj.GetOrdinal(strIndex)) Then
                    result = Obj.GetValue(Obj.GetOrdinal(strIndex))
                Else
                    result = Nothing
                End If
                Return True
            Else
                result = Nothing
                Return False
            End If
        Catch ex As Exception
            result = Nothing
            Return False
        End Try
    End Function

    Public Shared Widening Operator CType(ByVal reader As DynamicDataReader) As DbDataReader
        Return reader.Obj
    End Operator

    Public Shared Narrowing Operator CType(ByVal reader As DbDataReader) As DynamicDataReader
        Return New DynamicDataReader(reader)
    End Operator

End Class

Firstly, note how we override the TryGetMember() method again. In this override we first call the base method to test whether the member exists on the wrapped object. If it does, we return its value; otherwise, we attempt to read the appropriate column from the wrapped data reader. If this fails (i.e.: the named column does not exist) the method returns false, causing a RuntimeBinderException to be thrown at run-time.

Secondly, we also provide an override for the TryGetIndex() method, which allows us to retrieve column data using an indexer.

Also, note how we also provide two conversion operators for converting between the original object and its wrapper. This allows us to easily use our dynamic wrapper object in places where the framework is expecting the original wrapped object; and to quickly wrap an unwrapped object.

Some Examples

Here are some examples of our DynamicDataReader in action. All the examples use the Northwind database and are designed to show the DynamicDataReader in action. They are certainly not intended as an example of data-access best-practice (embedded SQL being a case in point)! Firstly, a custom HTTP handler to render a table of employees:

// C#
public class DataReaderExample : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "text/html";
        context.Response.Write("<html><body>");
        context.Response.Write("<h1>Employees:</h1>");
        context.Response.Write("<table><thead><tr><th>Employee Number</th><th>Surname</th><th>First Name</th><th>Date of Birth</th></tr></thead><tbody>");
        using (SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString))
        {
            string query = "SELECT EmployeeID, LastName, FirstName, Title, TitleOfCourtesy, BirthDate, HireDate, Address, City, Region, PostalCode, Country, HomePhone, Extension, Photo, Notes, ReportsTo, PhotoPath " +
                           "FROM dbo.Employees";
            SqlCommand command = new SqlCommand(query, connection);
            connection.Open();
            dynamic reader = (DynamicDataReader)command.ExecuteReader();
            while (reader.Read())
            {
                int employeeId = reader.EmployeeID;
                string lastName = reader.LastName;
                string firstName = reader.FirstName;
                DateTime birthDate = reader.BirthDate;
                context.Response.Write(String.Format("<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3:dd/MM/yyyy}</td></tr>", employeeId, lastName, firstName, birthDate));
            }
        }
        context.Response.Write("</tbody></table></body></html>");
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}
' Visual Basic
Public Class DataReaderExample
    Implements System.Web.IHttpHandler

    Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
        context.Response.ContentType = "text/html"
        context.Response.Write("<html><body>")
        context.Response.Write("<h1>Employees:</h1>")
        context.Response.Write("<table><thead><tr><th>Employee Number</th><th>Surname</th><th>First Name</th><th>Date of Birth</th></tr></thead><tbody>")
        Using connection As SqlConnection = New SqlConnection(WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString)
            Dim query As String = "SELECT EmployeeID, LastName, FirstName, Title, TitleOfCourtesy, BirthDate, HireDate, Address, City, Region, PostalCode, Country, HomePhone, Extension, Photo, Notes, ReportsTo, PhotoPath " + _
                                  "FROM dbo.Employees"
            Dim command As SqlCommand = New SqlCommand(query, connection)
            connection.Open()
            Dim reader As Object = CType(command.ExecuteReader(), DynamicDataReader)
            While (reader.Read())
                Dim employeeId As Integer = reader.EmployeeID
                Dim lastName As String = reader.LastName
                Dim firstName As String = reader.FirstName
                Dim birthDate As DateTime = reader.BirthDate
                context.Response.Write(String.Format("<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3:dd/MM/yyyy}</td></tr>", employeeId, lastName, firstName, birthDate))
            End While
        End Using
        context.Response.Write("</tbody></table></body></html>")
    End Sub

    ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
        Get
            Return False
        End Get
    End Property

End Class

As our DynamicDataReader class implements IEnumerable, it can be used with the standard ASP.NET data controls. In this example, it is used as the data source for a GridView:


<form id="form1" runat="server">
<div>
    <h1>
        GridView Examples</h1>
    <h2>
        Using DynamicDataReader</h2>
    <asp:GridView ID="dataReaderGridView" runat="server" />    
</div>
</form>
// C#
private void BindDataReader()
{
    using (SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString))
    {
        string query = "SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax " +
                       "FROM dbo.Customers";
        SqlCommand command = new SqlCommand(query, connection);                
        connection.Open();
        dynamic reader = (DynamicDataReader)command.ExecuteReader();
        dataReaderGridView.DataSource = reader;
        dataReaderGridView.DataBind();
    }
}
' Visual Basic
Private Sub BindDataReader()
    Using connection As SqlConnection = New SqlConnection(WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString)
        Dim query As String = "SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax " + _
                              "FROM dbo.Customers"
        Dim command As SqlCommand = New SqlCommand(query, connection)
        connection.Open()
        Dim reader As Object = CType(command.ExecuteReader(), DynamicDataReader)
        dataReaderGridView.DataSource = reader
        dataReaderGridView.DataBind()
    End Using
End Sub

However, in most real-world, multi-tier applications, we are probably most likely to be filling an object, or collection of objects, for use in the higher-level tiers of our system. The final example here shows the use of the DynamicDataReader class to fill an array of Customer objects:

// C#
public static Customer[] GetCustomers()
{
    List<Customer> customers = new List<Customer>();
    using (SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString))
    {
        string query = "SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax " +
                       "FROM dbo.Customers";
        SqlCommand command = new SqlCommand(query, connection);
        connection.Open();
        dynamic reader = (DynamicDataReader)command.ExecuteReader();
        while (reader.Read())
        {
            Customer customer = new Customer()
            {
                Id = reader.CustomerID,
                Company = reader.CompanyName,
                Name = reader.ContactName,
                Title = reader.ContactTitle,
                Address = reader.Address,
                City = reader.City,
                Region = reader.Region,
                PostCode = reader.PostalCode,
                Country = reader.Country,
                Phone = reader.Phone,
                Fax = reader.Fax
            };
            customers.Add(customer);
        }
        return customers.ToArray();
    }
}
' Visual Basic
Public Shared Function GetCustomers() As Customer()
    Dim customers As List(Of Customer) = New List(Of Customer)
    Using connection As SqlConnection = New SqlConnection(WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString)
        Dim query As String = "SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax " + _
                              "FROM dbo.Customers"
        Dim command = New SqlCommand(query, connection)
        connection.Open()
        Dim reader As Object = CType(command.ExecuteReader(), DynamicDataReader)
        While reader.Read()
            Dim customer As Customer = New Customer() With _
                                       { _
                                           .Id = reader.CustomerID, _
                                           .Company = reader.CompanyName, _
                                           .Name = reader.ContactName, _
                                           .Title = reader.ContactTitle, _
                                           .Address = reader.Address, _
                                           .City = reader.City, _
                                           .Region = reader.Region, _
                                           .PostCode = reader.PostalCode, _
                                           .Country = reader.Country, _
                                           .Phone = reader.Phone, _
                                           .Fax = reader.Fax _
                                       }
            customers.Add(customer)
        End While
    End Using
    Return customers.ToArray()
End Function

Limitations

  • If a database column has the same name as a propetry of the wrapped DbDataReader class, it cannot be accessed using the dynamic properties of the wrapper. Instead the column can be accessed via the indexer on the wrapper.
  • If the application is performance-critical, you may want to consider removing the reflection calls to the wrapped object in the TryGetMember() and TrySetMember() of the base DynamicDataObjectWrapper class.

Summary

The DynamicDataReader provides a means of retrieving data from a database in a way that involves less code and increases code readability.

The source code will be available for download with Part II.

Next: The DynamicDataSet

Monday, 16 May 2011

Unit Testing 101: Getting Started with NUnit


Introduction

Unit testing is the process of using short, programmatic tests to test the logic and functionality of discreet units of code. The use of such tests brings a whole host of benefits, not least facilitating change within the code base (including refactoring), by being able to easily identify if breaking-changes have been introduced; as well encouraging the use of test-driven development.

NUnit is a well-established, open-source unit testing framework for .NET. It is, in my opinion, extremely easy to use; and is very well supported by most good continuous-integration systems, allowing unit tests to be run automatically as part of the build process.

This article is primarily for those of you who are new to unit testing and is intended as a basic introduction to unit testing and test-driven development; as well as how to write basic tests using the NUnit framework.

Our Scenario

The scenario we are going to look at in this article involves a simple BankAccount class. This class has three methods:

MethodDescription
Deposit()Deposits money into the account
Withdraw()Withdraws money from the account. Throws an exception if there are insufficient funds to make the withdrawal.
Transfer()Transfers money to another account. Throws an exception if there are insufficient funds to make the transfer.

Our Tests

With this scenario in mind, I have written the following unit tests to test the individual functionality of each of these methods:

// C#
[TestFixture]
public sealed class BankAccountTestFixture
{
    private BankAccount bankAccountA;
    private BankAccount bankAccountB;

    [SetUp]
    public void SetUp()
    {
        bankAccountA = new BankAccount(100.00);
        bankAccountB = new BankAccount(20.00);
    }

    [Test]
    public void Deposit()
    {
        bankAccountA.Deposit(10.00);
        Assert.That(bankAccountA.Balance, Is.EqualTo(110.00));
    }

    [Test]
    public void Withdraw()
    {
        bankAccountA.Withdraw(10.00);
        Assert.That(bankAccountA.Balance, Is.EqualTo(90.00));
    }

    [Test]
    public void WithdrawWithInsufficentFunds()
    {
        Assert.That(() => bankAccountB.Transfer(30.00, bankAccountA), Throws.InstanceOf<OverdrawnException>());
        Assert.That(bankAccountA.Balance, Is.EqualTo(100.00));
    }

    [Test]
    public void Transfer()
    {
        bankAccountA.Transfer(20.00, bankAccountB);
        Assert.That(bankAccountA.Balance, Is.EqualTo(80.00));
        Assert.That(bankAccountB.Balance, Is.EqualTo(40.00));
    }

    [Test]
    public void TransferWithInsufficientFunds()
    {
        Assert.That(() => bankAccountB.Transfer(30.00, bankAccountA), Throws.InstanceOf<OverdrawnException>());
        Assert.That(bankAccountA.Balance, Is.EqualTo(100.00));
        Assert.That(bankAccountB.Balance, Is.EqualTo(20.00));
    }
}
' Visual Basic
<TestFixture()>
Public NotInheritable Class BankAccountTestFixture

    Private _bankAccountA As BankAccount
    Private _bankAccountB As BankAccount

    <SetUp()>
    Public Sub SetUp()
        _bankAccountA = New BankAccount(100.0)
        _bankAccountB = New BankAccount(20.0)
    End Sub

    <Test()>
    Public Sub Deposit()
        _bankAccountA.Deposit(10.0)
        Assert.That(_bankAccountA.Balance, [Is].EqualTo(110.0))
    End Sub

    <Test()>
    Public Sub Withdraw()
        _bankAccountA.Withdraw(10.0)
        Assert.That(_bankAccountA.Balance, [Is].EqualTo(90.0))
    End Sub

    <Test()>
    Public Sub WithdrawWithInsufficientFunds()
        Assert.That(Sub() _bankAccountB.Transfer(30.0, _bankAccountA), Throws.InstanceOf(Of OverdrawnException)())
        Assert.That(_bankAccountA.Balance, [Is].EqualTo(100.0))
    End Sub

    <Test()>
    Public Sub Transfer()
        _bankAccountA.Transfer(20.0, _bankAccountB)
        Assert.That(_bankAccountA.Balance, [Is].EqualTo(80.0))
        Assert.That(_bankAccountB.Balance, [Is].EqualTo(40.0))
    End Sub

    <Test()>
    Public Sub TransferWithInsufficientFunds()
        Assert.That(Sub() _bankAccountB.Transfer(30.0, _bankAccountA), Throws.InstanceOf(Of OverdrawnException)())
        Assert.That(_bankAccountA.Balance, [Is].EqualTo(100.0))
        Assert.That(_bankAccountB.Balance, [Is].EqualTo(20.0))
    End Sub
End Class

Now let's examine this code a little more closely. Firstly, you will have probably noticed the liberal use of attribute decoration. These attributes serve to identify each of the component parts of our test suite to NUnit. The TestFixture attribute informs NUnit that the class it is decorating contains one or more tests to be run. The SetUp attribute identifies a method which is to be executed before each test is run. In our example, we use this to ensure that both our bank accounts always have the same starting balances prior to the execution of each test:

// C#
[SetUp]
public void SetUp()
{
    bankAccountA = new BankAccount(100.00);
    bankAccountB = new BankAccount(20.00);
}
' Visual Basic
<SetUp()>
Public Sub SetUp()
    _bankAccountA = New BankAccount(100.0)
    _bankAccountB = New BankAccount(20.0)
End Sub

There is also a corresponding TearDown attribute which identifies a method which is to be executed after each test is run. Although we don't use this in our example, it generally used for performing any clean-up operations.

Finally, the Test attribute identifies each of the individual tests. Let's have a look at one of our tests more closely:

// C#
[Test]
public void TransferWithInsufficientFunds()
{
    Assert.That(() => bankAccountB.Transfer(30.00, bankAccountA), Throws.InstanceOf<OverdrawnException>());
    Assert.That(bankAccountA.Balance, Is.EqualTo(100.00));
    Assert.That(bankAccountB.Balance, Is.EqualTo(20.00));
}
' Visual Basic
<Test()>
Public Sub TransferWithInsufficientFunds()
    Assert.That(Sub() _bankAccountB.Transfer(30.0, _bankAccountA), Throws.InstanceOf(Of OverdrawnException)())
    Assert.That(_bankAccountA.Balance, [Is].EqualTo(100.0))
    Assert.That(_bankAccountB.Balance, [Is].EqualTo(20.0))
End Sub

The Assert.That() method is used to assert that certain conditions have been satisfied in order to pass the test. In our first assertion, we are asserting that an exception of type OverdrawnException is thrown when we make a call to the Transfer() method of the BankAccount object. The second and third assertions are asserting that the Balance properties of the two bank accounts are equal to 100.00 and 20.00 respectively.

Running our Tests (and Fixing our Code)

So what happens when we run our tests? NUnit provides a simple WinForms app for running unit tests: We simply load our dll and pick which test(s) we wish to run. The screenshot below shows the output of our tests:

As you can see, three of our five tests fail. This is because, in true test-driven development style, I have written the tests first and I am now only part-way through writing the implementation of the BankAccount class. Here is the code so far:

// C#
public sealed class BankAccount
{
    public BankAccount()
        : this(0)
    {
    }

    public BankAccount(double initialBalance)
    {
        Balance = initialBalance;
    }

    public double Balance { get; private set; }

    public void Deposit(double amount)
    {
        Balance += amount;
    }

    public void Withdraw(double amount)
    {
        Balance -= amount;
    }

    public void Transfer(double amount, BankAccount destination)
    {
        //TODO:  Do some stuff here.
    }
}
' Visual Basic
Public NotInheritable Class BankAccount

    Private _balance As Double

    Public Sub New()
        Me.New(0)
    End Sub

    Public Sub New(ByVal initialBalance As Double)
        _balance = initialBalance
    End Sub

    Public ReadOnly Property Balance As Double
        Get
            Return _balance
        End Get
    End Property

    Public Sub Deposit(ByVal amount As Double)
        _balance = _balance + amount
    End Sub

    Public Sub Withdraw(ByVal amount As Double)
        _balance = _balance - amount
    End Sub

    Public Sub Transfer(ByVal amount As Double, ByVal destination As BankAccount)
        ' TODO: Do some stuff here.
    End Sub
End Class

As you can see, there is no implementation at all for the Transfer() method. Let's provide one now:

// C#
public void Transfer(double amount, BankAccount destination)
{
    this.Withdraw(amount);
    destination.Deposit(amount);
}
' Visual Basic
Public Sub Transfer(ByVal amount As Double, ByVal destination As BankAccount)
    Me.Withdraw(amount)
    destination.Deposit(amount)
End Sub

If we re-run the tests, we can now see the Transfer() test now passes:

However, our tests are still failing when we have insufficient funds in our account to make either a withdrawal or a transfer. A simple modification to our Withdraw() method should now fix both of these:

// C#
public void Withdraw(double amount)
{
    if (Balance >= amount)
        Balance -= amount;
    else
        throw new OverdrawnException("You have insufficient funds in the account.");
}
' Visual Basic
Public Sub Withdraw(ByVal amount As Double)
    If _balance >= amount Then
        _balance = _balance - amount
    Else
        Throw New OverdrawnException("You have insufficient funds in the account.")
    End If
End Sub

Now, all our tests should be green when we run them again:

Summary

Unit testing is a highly useful and important tool when working on development projects of all sizes. It provides a degree of confidence that the code being developed is fit-for-purpose and does not break any existing functionality of the system.

With the NUnit framework, it is very simple to write unit tests for test-driven development scenarios.

Further Reading

Friday, 13 May 2011

A Simple Wrapper for log4net

Introduction

log4net is a highly flexible logging framework available for .NET. Now firstly, let me say that this article assumes a basic familiarity with the log4net framework, and is in not intended either as an introduction to log4net or a tutorial in how to configure and use log4net.

If you are unfamiliar with log4net, you can find the documentation here, or have a look at this article on CodeProject.

The Problem

One of my main criticisms of log4net is that I find the syntax somewhat clumsy. Take, for example, the following code to add a line of simple debugging logging:

// C#
ILog log = LogManager.GetLogger("MyLog");
if (log.IsDebugEnabled)
{
    using (ThreadContext.Stacks["MyProperty"].Push("MyValue"))
    {
        using(ThreadContext.Stacks["MyOtherProperty"].Push("MyOtherValue"))
        {
            log.Debug("This is a debugging message.");
        }
    }
}
' Visual Basic
Dim log As ILog = LogManager.GetLogger("MyLog")
If log.IsDebugEnabled Then
    Using ThreadContext.Stacks("MyProperty").Push("MyValue")
        Using ThreadContext.Stacks("MyOtherProperty").Push("MyOtherValue")
            log.Debug("This is a debugging message.")
        End Using
    End Using
End If

As you can see, performing what should be a simple logging operation takes up about half-a-dozen lines of code. Clearly for even the smallest application, littering the code with statements like those just shown, simply for logging purposes, is not exactly desirable and would soon become unreadable as the logging code starts to drown out the logic code.

What is needed is a solution whereby a logging operation can be accomplished with a single line of code.

The Solution

My solution is a static class, called Logger, which has two methods (plus overloads): Initialize() to initialize log4net, and Log() which actually performs the logging operation

Firstly, let's look at the Initialize() method. This has two overloads: one takes a single parameter which specifies the config file to be used by log4net. The parameterless overload specifies that log4net should pull its configuration from web.config or app.config:

// C#
private static bool isInitialized;

public static void Initialize()
{
    Initialize(null);
}

public static void Initialize(string configFile)
{
    if (!isInitialized)
    {
        if (!String.IsNullOrEmpty(configFile))
            XmlConfigurator.ConfigureAndWatch(new FileInfo(configFile));
        else
            XmlConfigurator.Configure();
        isInitialized = true;
    }
    else
        throw new LoggingInitializationException(
              "Logging has already been initialized.");
}

' Visual Basic
Private _isInitialized As Boolean

Public Sub Initialize()
    Initialize(Nothing)
End Sub

Public Sub Initialize(ByVal configFile As String)
    If Not _isInitialized Then
        If Not String.IsNullOrEmpty(configFile) Then
            XmlConfigurator.ConfigureAndWatch(New FileInfo(configFile))
        Else
            XmlConfigurator.Configure()
        End If
        _isInitialized = True
    Else
        Throw New LoggingInitializationException("Logging has already been initialized.")
    End If
End Sub

Next, we'll look at the Log() method and its overloads. These methods accept various combinations of the following parameters:

Parameter Description
logName The name of the log to write the event to. If omitted, the event is written to all logs.
loggingLevel The level (debug, info, warning, error or fatal) of the event.
message The message.
loggingProperties An object whose properties map to log4net PatternLayout properties (e.g.: %property{Foo} where Foo is the name of the property).
exception The exception (if any) to be logged.

All of the Log() method overloads ultimately call LogBase() which is where the actual calls to log4net are made. The code for this method is as follows:

// C#
private static void LogBase(ILog log, LoggingLevel loggingLevel, 
        string message, object loggingProperties, Exception exception)
{
    if (ShouldLog(log, loggingLevel))
    {
        PushLoggingProperties(loggingProperties);
        switch (loggingLevel)
        {
            case LoggingLevel.Debug: log.Debug(message, exception); break;
            case LoggingLevel.Info: log.Info(message, exception); break;
            case LoggingLevel.Warning: log.Warn(message, exception); break;
            case LoggingLevel.Error: log.Error(message, exception); break;
            case LoggingLevel.Fatal: log.Fatal(message, exception); break;
        }
        PopLoggingProperties(loggingProperties);
    }
}
' Visual Basic
Private Sub LogBase(ByVal log As ILog, ByVal loggingLevel As LoggingLevel, _
        ByVal message As String, ByVal loggingProperties _
        As Object, ByVal exception As Exception)
    If ShouldLog(log, loggingLevel) Then
        PushLoggingProperties(loggingProperties)
        Select Case loggingLevel
            Case VisualBasic.LoggingLevel.Debug
                log.Debug(message, exception)
            Case VisualBasic.LoggingLevel.Info
                log.Info(message, exception)
            Case VisualBasic.LoggingLevel.Warning
                log.Warn(message, exception)
            Case VisualBasic.LoggingLevel.Error
                log.Error(message, exception)
            Case VisualBasic.LoggingLevel.Fatal
                log.Fatal(message, exception)
        End Select
        PopLoggingProperties(loggingProperties)
    End If
End Sub

As you can see, the first call made by this method is to the ShouldLog() method, which determines whether or not the event should be logged, based on the current logging level:

// C#
private static bool ShouldLog(ILog log, LoggingLevel loggingLevel)
{
    switch (loggingLevel)
    {
        case LoggingLevel.Debug: return log.IsDebugEnabled;
        case LoggingLevel.Info: return log.IsInfoEnabled;
        case LoggingLevel.Warning: return log.IsWarnEnabled;
        case LoggingLevel.Error: return log.IsErrorEnabled;
        case LoggingLevel.Fatal: return log.IsFatalEnabled;
        default: return false;
    }
}
' Visual Basic
Private Function ShouldLog(ByVal log As ILog, _
        ByVal loggingLevel As LoggingLevel) As Boolean
    Select Case loggingLevel
        Case VisualBasic.LoggingLevel.Debug
            Return log.IsDebugEnabled
        Case VisualBasic.LoggingLevel.Info
            Return log.IsInfoEnabled
        Case VisualBasic.LoggingLevel.Warning
            Return log.IsWarnEnabled
        Case VisualBasic.LoggingLevel.Error
            Return log.IsErrorEnabled
        Case VisualBasic.LoggingLevel.Fatal
            Return log.IsFatalEnabled
        Case Else
            Return False
    End Select
End Function

If this method returns true, the next method to be called is PushLoggingProperties(). This takes the loggingProperties parameter and uses Reflection to push the values of each of the properties onto the log4net ThreadContext stack:

// C#
private static void PushLoggingProperties(object loggingProperties)
{
    if (loggingProperties != null)
    {
        Type attrType = loggingProperties.GetType();
        PropertyInfo[] properties = attrType.GetProperties(
                       BindingFlags.Public | BindingFlags.Instance);
        for (int i = 0; i < properties.Length; i++)
        {
            object value = properties[i].GetValue(loggingProperties, null);
            if (value != null)
                ThreadContext.Stacks[properties[i].Name].Push(value.ToString());
        }
    }
}
' Visual Basic
Private Sub PushLoggingProperties(ByVal loggingProperties As Object)
    If Not loggingProperties Is Nothing Then
        Dim attrType As Type = loggingProperties.GetType()
        Dim properties() As PropertyInfo = attrType.GetProperties(
                         BindingFlags.Public Or BindingFlags.Instance)
        For i As Integer = 0 To properties.Length - 1 Step +1
            Dim value As Object = properties(i).GetValue(loggingProperties, Nothing)
            If Not value Is Nothing Then
                ThreadContext.Stacks(properties(i).Name).Push(value.ToString())
            End If
        Next
    End If
End Sub

Next, the code calls the appropriate logging method on the log4net ILog object, based on the current logging level. This is followed by a call to the PopLoggingProperties() which simply performs the reverse of PushLoggingProperties() and pops the properties off the stack.

Some Examples

Here are some examples of the Logger class in action. In these examples, we are using the following PatternLayout:

<layout type="log4net.Layout.PatternLayout">
    <conversionPattern 
     value="%date [%thread] %-5level %logger [%property{User}]: 
            %{property{Environment}  - %message%newline%exception" />
</layout>
// C#
// Initialise log4net
Logger.Initialize();

// Write a debugging event to all logs.
Logger.Log(LoggingLevel.Debug, "Calling method Foo().", 
           new { User = "Joe Bloggs", Environment = "UAT" });

// Write an exception to the log called "ErrorLog".
try
{
    int i = 25;
    int j = 0;
    int foo = i / j;
}
catch (DivideByZeroException ex)
{
    Logger.Log("ErrorLog", LoggingLevel.Error, "Attempted to divide by zero.",
    new { User = "Fred Bloggs", Environment = "Production" }, ex);
}
' Visual Basic
' Initialise log4net
Logger.Initialize()

' Write a debugging event to all logs.
Logger.Log(LoggingLevel.Debug, "Calling method Foo().", _
   New With {.User = "Joe Bloggs", .Environment = "UAT"})

' Write an exception to the log called "ErrorLog".
Try
    Dim i As Integer = 25
    Dim j As Integer = 0
    Dim foo As Integer = CInt(i / j)
Catch ex As DivideByZeroException
    Logger.Log("ErrorLog", LoggingLevel.Error, "Attempted to divide by zero.", _
    New With {.User = "Fred Bloggs", .Environment = "Production"}, ex)
End Try

Summary

With a simple wrapper, it is possible abstract away the code for interacting with the log4net framework, and at the same time improve the readability of our code by reducing logging operations to single lines.

You can download the complete code from here.

Saturday, 7 May 2011

Rounding Floating-Point Numbers Up or Down in .NET


Introduction

Yesterday, I needed to be able not only to round a floating-point number to a certain number of decimal places, but also control the direction the rounding (i.e.: always force it to round down).

Not a problem, I thought. There will be a method on the System.Math class that will let me do just that. How wrong can you be?? My first port of call was the Math.Round() method, but whilst you can specify the number of decimal places with this method, you cannot force it to always round either up or down. Next, I looked at Math.Floor() and Math.Ceiling() methods. These, however, will only round to the nearest integer.

There was nothing else for it, but to roll my own.

The Solution

The code for my solution is as follows:

// C#
public static class EnhancedMath
{
    private delegate double RoundingFunction(double value);

    private enum RoundingDirection { Up, Down }

    public static double RoundUp(double value, int precision)
    {
        return Round(value, precision, RoundingDirection.Up);
    }

    public static double RoundDown(double value, int precision)
    {
        return Round(value, precision, RoundingDirection.Down);
    }

    private static double Round(double value, int precision, RoundingDirection roundingDirection)
    {
        RoundingFunction roundingFunction;
        if (roundingDirection == RoundingDirection.Up)
            roundingFunction = Math.Ceiling;
        else
            roundingFunction = Math.Floor;
        value *= Math.Pow(10, precision);
        value = roundingFunction(value);
        return value * Math.Pow(10, -1 * precision);
    }
}
' Visual Basic
Public Module EnhancedMath

    Private Delegate Function RoundingFunction(ByVal value As Double) As Double

    Private Enum RoundingDireciton
        Up
        Down
    End Enum

    Public Function RoundUp(ByVal value As Double, ByVal precision As Integer) As Double
        Return Round(value, precision, RoundingDireciton.Up)
    End Function

    Public Function RoundDown(ByVal value As Double, ByVal precision As Integer) As Double
        Return Round(value, precision, RoundingDireciton.Down)
    End Function

    Private Function Round(ByVal value As Double, ByVal precision As Integer, ByVal roundingDirection As RoundingDireciton) As Double
        Dim roundingFunction As RoundingFunction
        If roundingDirection = RoundingDireciton.Up Then
            roundingFunction = AddressOf Math.Ceiling
        Else
            roundingFunction = AddressOf Math.Floor
        End If
        value = value * Math.Pow(10, precision)
        value = roundingFunction(value)
        Return value * Math.Pow(10, -1 * precision)
    End Function

End Module

The rounding algorithm is quite simple: Firstly we shift all the digits we are interested in keeping, to the left of the decimal point. We then either call Math.Floor() or Math.Ceiling() on the result depending on whether we are rounding down or up respectively. Finally, we shift our digits back to the right of the decimal point and return the value.

The method handles values of type Double. You can always add your own overloads to handle values of other types(e.g.: Decimal).

Some Examples

Here are some examples of our code in action:

// C#
static void Main(string[] args)
{
    Console.WriteLine(EnhancedMath.RoundUp(3.141592654, 2)); // Result: 3.15
    Console.WriteLine(EnhancedMath.RoundUp(3.141592654, 3)); // Result: 3.142
    Console.WriteLine(EnhancedMath.RoundDown(3.141592654, 2)); // Result: 3.14
    Console.WriteLine(EnhancedMath.RoundDown(3.141592654, 3)); // Result: 3.141
    Console.Read();
}
' Visual Basic
Sub Main(ByVal args() As String)
    Console.WriteLine(EnhancedMath.RoundUp(3.141592654, 2)) ' Result: 3.15
    Console.WriteLine(EnhancedMath.RoundUp(3.141592654, 3)) ' Result: 3.142
    Console.WriteLine(EnhancedMath.RoundDown(3.141592654, 2)) ' Result: 3.14
    Console.WriteLine(EnhancedMath.RoundDown(3.141592654, 3)) ' Result: 3.141
    Console.Read()
End Sub

Friday, 6 May 2011

Delegates 101 - Part IV: Event Handling


Introduction

So, here we are at the final part of this series on delegates; and it wouldn't be complete without looking at one of the key uses of delegates in .NET: Event handling.

A Simple Timer

In this article, we are going to look at a simple timer example. This timer raises two events: one when the timer is started and another when the timer has elapsed. The complete code for the timer is as follows:

// C#
public delegate void TimerEventHandler(object sender, EventArgs eventArgs);

public class SimpleTimer
{
    private TimeSpan time;

    public event TimerEventHandler Started;
    public event TimerEventHandler Elapsed;

    public SimpleTimer(long milliseconds)
        : this(TimeSpan.FromMilliseconds(milliseconds))
    {
    }

    public SimpleTimer(TimeSpan time)
    {
        this.time = time;
    }

    public void Start()
    {
        Thread timerThread = new Thread
        (
            delegate()
            {
                OnStarted(new EventArgs());
                Thread.Sleep(time);
                OnElapsed(new EventArgs());
            }
        );
        timerThread.Start();
    }

    private void OnStarted(EventArgs eventArgs)
    {
        if (Started != null)
            Started(this, eventArgs);
    }

    private void OnElapsed(EventArgs eventArgs)
    {
        if (Elapsed != null)
            Elapsed(this, eventArgs);
    }
}
' Visual Basic
Public Delegate Sub TimerEventHandler(ByVal sender As Object, ByVal eventArgs As EventArgs)

Public Class SimpleTimer

    Private _time As TimeSpan

    Public Event Started As TimerEventHandler
    Public Event Elapsed As TimerEventHandler

    Public Sub New(ByVal milliseconds As Long)
        Me.New(TimeSpan.FromMilliseconds(milliseconds))
    End Sub

    Public Sub New(ByVal time As TimeSpan)
        _time = time
    End Sub

    Public Sub Start()
        Dim timerThread As Thread = New Thread _
        ( _
            Sub()
                OnStarted(New EventArgs())
                Thread.Sleep(_time)
                OnElapsed(New EventArgs())
            End Sub _
        )
        timerThread.Start()
    End Sub

    Private Sub OnStarted(ByVal eventArgs As EventArgs)
        RaiseEvent Started(Me, New EventArgs())
    End Sub

    Private Sub OnElapsed(ByVal eventArgs As EventArgs)
        RaiseEvent Elapsed(Me, New EventArgs())
    End Sub
End Class

Now let's look at this code a little more closely. Firstly, notice how we declare a delegate for any methods which can be used to handle our two events:

// C#
public delegate void TimerEventHandler(object sender, EventArgs eventArgs);
' Visual Basic
Public Delegate Sub TimerEventHandler(ByVal sender As Object, ByVal eventArgs As EventArgs)

By convention, event handlers accept two parameters:

ParameterDescription
senderThe object which raised the event.
eventArgsAn object of type EventArgs, or inherits from EventArgs, which contains encapsulates any optional data we wish to pass to our event handler

We also declare the two events which out timer is going to raise:

// C#
public event TimerEventHandler Started;
public event TimerEventHandler Elapsed;
' Visual Basic
Public Event Started As TimerEventHandler
Public Event Elapsed As TimerEventHandler

Notice how the type of each of these events is that of the delegate which is going to handle them, in this case TimerEventHandler.

We also have convenience methods for raising each of the events:

// C#
private void OnStarted(EventArgs eventArgs)
{
    if (Started != null)
        Started(this, eventArgs);
}

private void OnElapsed(EventArgs eventArgs)
{
    if (Elapsed != null)
        Elapsed(this, eventArgs);
}
' Visual Basic
Private Sub OnStarted(ByVal eventArgs As EventArgs)
    RaiseEvent Started(Me, New EventArgs())
End Sub

Private Sub OnElapsed(ByVal eventArgs As EventArgs)
    RaiseEvent Elapsed(Me, New EventArgs())
End Sub

Note how we fire the event in exactly the same way as we would call any other delegate. Additionally, in C#, we do a check to see if any handlers have been wired-up up to our event; as firing an event without any handlers will cause an exception to be thrown.

Finally, the code which actually runs our timer and raises the events. Note how the timer is executed in a separate thread and (simply because it is in-keeping with the theme of this series) is passed to the thread as an anonymous method:

// C#
public void Start()
{
    Thread timerThread = new Thread
    (
        delegate()
        {
            OnStarted(new EventArgs());
            Thread.Sleep(time);
            OnElapsed(new EventArgs());
        }
    );
    timerThread.Start();
}
' Visual Basic
Public Sub Start()
    Dim timerThread As Thread = New Thread _
    ( _
        Sub()
            OnStarted(New EventArgs())
            Thread.Sleep(_time)
            OnElapsed(New EventArgs())
        End Sub _
    )
    timerThread.Start()
End Sub

Using our Timer

The following code shows how to use our simple timer and hook up our own methods to handle the events it raises:

// C#
static void Main(string[] args)
{
    SimpleTimer timer = new SimpleTimer(5000);
    timer.Started += timer_Started;
    timer.Elapsed += timer_Elapsed;
    timer.Start();
    Console.Read();
}

static void timer_Started(object sender, EventArgs e)
{
    Console.WriteLine("Timer has started.");
}

static void timer_Elapsed(object sender, EventArgs e)
{
    Console.WriteLine("Timer has elapsed.");
}
' Visual Basic
Sub Main(ByVal args() As String)
    Dim timer As SimpleTimer = New SimpleTimer(5000)
    AddHandler timer.Started, AddressOf timer_Started
    AddHandler timer.Elapsed, AddressOf timer_Elapsed
    timer.Start()
    Console.Read()
End Sub

Private Sub timer_Started(sender As Object, eventArgs As EventArgs)
    Console.WriteLine("Timer has started.")
End Sub

Private Sub timer_Elapsed(sender As Object, eventArgs As EventArgs)
    Console.WriteLine("Timer has elapsed.")
End Sub

Note how the syntax for wiring up our event handlers to the events is slightly different from what we've seen previously. This is because events are a type of multicast delegate, and as such multiple handlers can be wired up the same event.

Summary

Handling of events is one of the key uses for delegates in .NET. All event handlers follow a specific convention in terms of their signature and multiple different handlers can be wired up to the same event.

Thursday, 5 May 2011

Delegates 101 - Part III: Generic Delegates


Introduction

In Part II we looked at how we can use anonymous methods and lambdas to pass in-line method implementations to a method which takes a delegate as a parameter.

In this article, we will look at how we can leverage the power of generics in order to make our delegates more ...well ...generic!

Generic Delegates

Continuing with our maths theme, let's look again at our original MathFunction delegate:

// C#
delegate int MathFunction(int int1, int int2);
' Visual Basic
Delegate Function MathFunction(ByVal int1 As Integer, ByVal int2 As Integer) As Integer

Well that is fine, providing all we ever want to work with are integers! In the real world, however, we are probably going to want to perform mathematical operations on all sorts of different data types. By use of a generic delegate, we can have a single delegate which will handle any data type we like in a type-safe manner:

// C#
delegate TResult MathFunction<T1, T2, TResult>(T1 var1, T2 var2);
' Visual Basic
Delegate Function MathFunction(Of T1, T2, TResult)(ByVal var1 As T1, ByVal var2 As T2) As TResult

Our delegate will now accept parameters of any arbitrary type and return a result of any arbitrary type.

Next, we need to modify our PrintResult() method accordingly to handle our new delegate:

// C#
static void PrintResult<T1, T2, TResult>(MathFunction<T1, T2, TResult> mathFunction, T1 var1, T2 var2)
{
    TResult result = mathFunction(var1, var2);
    Console.WriteLine(String.Format("Result is {0}", result));
}
' Visual Basic
Sub PrintResult(Of T1, T2, TResult)(ByVal mathFunction As MathFunction(Of T1, T2, TResult), ByVal var1 As T1, ByVal var2 As T2)
    Dim result As TResult = mathFunction(var1, var2)
    Console.WriteLine(String.Format("Result is {0}", result))
End Sub

Our simple calculator should now be able to perform mathematical operations on any data type of our choosing. Here are some examples:

// C#
PrintResult((x, y) => x / y, 5, 2); // Integer division - Result: 2
PrintResult((x, y) => x / y, 5, 2.0); // Real division - Result: 2.5
PrintResult((x, y) => 2 * y * x, 25, Math.PI); // Circumference of a circle - Result: 157.07963267949
PrintResult((x, y) => y * Math.Pow(x, 2), 25, Math.PI); // Area of circle - Result: 1963.49540849362
PrintResult((x, y) => (x - y).TotalDays, DateTime.Now, new DateTime(1940, 10, 9)); // Days since birth of John Lennon - Result: 25775.8028079865
' Visual Basic
PrintResult(Function(x, y) CInt(x / y), 5, 2) ' Integer division - Result: 2
PrintResult(Function(x, y) x / y, 5, 2.0) ' Real division - Result: 2.5
PrintResult(Function(x, y) 2 * y * x, 25, Math.PI) ' Circumference of a circle - Result: 157.07963267949
PrintResult(Function(x, y) y * Math.Pow(x, 2), 25, Math.PI) ' Area of circle - Result: 1963.49540849362
PrintResult(Function(x, y) (x - y).TotalDays, DateTime.Now, New DateTime(1940, 10, 9)) ' Days since birth of John Lennon - Result: 25775.8028079865

In each example, note how the types of x and y, as well as the return type of the lambda expression, are inferred by the compiler.

Delegate Re-use

Our MathFunction generic delegate can now be used as a type for any method that accepts two parameters of any type and returns a value of any type. As such, our delegate is highly re-usable and not just restricted to our simple calculator scenario.

Indeed, Microsoft have included a whole stack of generic Func and Action delegates in the System namespace which would probably cover most conceivable scenarios. As such, we could have used one of those in our example here; however that would have defeated the object of the exercise. I personally believe there is still a strong case for creating your own delegates in certain scenarios, as it often improves code readability.

Summary

Using generics allows us to write more versatile delegates which can be re-used for a whole variety of different scenarios. Even when using generic delegates, the compiler is still able to infer the types of any parameters and the return type.

Next: Event Handling