navigation
 Tuesday, January 30, 2007

First of all, that title was a mouthful, so let me describe what I meant. A custom DataSource is something that you can assign (or databind) to the DataSource property of any bindable control. In programming terms, this means that the DataSource must implement IListSource, IDataSource, or IEnumerable. Well, I don't know about you, but I don't really want to spend a lot of time implementing all the methods of any of those interfaces. That's where Iterators come in; C# 2.0 added a new feature called Iterator blocks that makes it extremely easy to implement the IEnumerator interface, which is the only thing that you must implement in order to fully implement IEnumerable. In this article, I'll solve a real-world problem using this technique.

In application development, it's very common to need to fill a list control such as a DropDownList with a list of valid items, plus a blank item which represents a null value in a data store. However, while it is very easy to simply set some properties on a DropDownList to databind its items to the contents of a DataTable, there is no simple way to add that blank value without dropping into the code-behind. This is no big deal, but it just doesn't feel very clean to fill some DropDownLists declaratively, and have to resort to imperative code for other DropDownLists. One way to solve this problem would be to create a custom control descending from each list control with properties that control the extra blank item, but first of all, you'd have to write a custom control for each list control, and secondly, if we did that, then we couldn't talk about Iterators, could we? So instead, I'm going to create a custom DataSource that returns the contents of a DataTable, plus a blank item to represent null.

Let's proceed with writing the custom DataSource. Using Iterators to implement IEnumerable is as easy as declaring a class that implements it and using the yield keyword to build the return value. Let's declare our ListSource class and let Visual Studio create the method stubs to implement the interface:

using System;
using System.Collections.Generic;
using System.Text;

namespace Falafel.ListData
{
  class ListDataSource : IEnumerable<ListData>
  {
    #region IEnumerable<ListData> Members

    IEnumerator<ListData> IEnumerable<ListData>.GetEnumerator()
    {
      throw new Exception( "The method or operation is not implemented." );
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
      throw new Exception( "The method or operation is not implemented." );
    }

    #endregion
  }
}

Assume the ListData class is a simple data class with two string properties: Text and Value. The first thing to notice is that the parameter list for GetEnumerator is empty, so the class itself will need to be initialized with properties that control the output of GetEnumerator. Let's add some private fields and a constructor to initialize them. New code is in italics:

using System;
using System.Collections.Generic;
using System.Data;
using System.Text;

namespace Falafel.ListData
{
  class ListDataSource : IEnumerable<ListData>
  {
    private DataTable _Table;
    private string _TextField;
    private string _ValueField;

    public ListDataSource( DataTable table, string textField, string valueField )
    {
      _Table = table;
      _TextField = textField;
      _ValueField = valueField;
    }


    #region IEnumerable<ListData> Members

    IEnumerator<ListData> IEnumerable<ListData>.GetEnumerator()
    {
      throw new Exception( "The method or operation is not implemented." );
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
      throw new Exception( "The method or operation is not implemented." );
    }

    #endregion
  }
}

Of course, you could add code to expose the private fields as properties, but since that's not the focus of the article, let's just assume everyone knows how to do that so we can get to the interesting part. Now that we can create an instance of the class with a reference to a DataTable and some field names, we can implement GetEnumerator:

using System;
using System.Collections.Generic;
using System.Data;
using System.Text;

namespace Falafel.ListData
{
  class ListDataSource : IEnumerable<ListData>
  {
    private DataTable _Table;
    private string _TextField;
    private string _ValueField;

    public ListDataSource( DataTable table, string textField, string valueField )
    {
      _Table = table;
      _TextField = textField;
      _ValueField = valueField;
    }

    #region IEnumerable<ListData> Members

    IEnumerator<ListData> IEnumerable<ListData>.GetEnumerator()
    {
      yield return new ListData( String.Empty, String.Empty );
      foreach ( DataRow row in _Table.Rows )
        yield return new ListData( (string) row[ _TextField ], (string) row[ _ValueField ] );

    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
      return ( (IEnumerable<ListData>) this ).GetEnumerator();
    }

    #endregion
  }
}

Let's pause for a minute and contemplate the power of what those two lines have. The return type of GetEnumerator is IEnumerator<ListData>, but nowhere are we implementing any of the methods of the IEnumerator interface. Instead, we are simply iterating through a collection and returning each value with yield return. The C# 2.0 compiler does the work of implementing the IEnumerable interface for you.

Well, you could add any bells and whistles to the class that you wanted to, of course. Maybe you want to be able to specify the text and value of the null item, or maybe you want access to those private members through properties. Let's fast-forward past all that and demonstrate the final application of this class: the ability to bind to the DataSource declaratively. To do so, open any aspx page in Source View, locate a list control such as a DropDownList, and add the following attributes to the opening tag:

DataSource='<%# new ListDataSource( ds.MyTable, "MyTextField", "MyValueField" ) %>' DataTextField="Text" DataValueField="Value"

Now, when the control is databound, it will bind to our custom DataSource and fill its items using the ListData objects yielded by GetEnumerator.

In this article, I took two concepts: that all it takes to implement a DataSource is to implement IEnumerable, and that all it takes to implement IEnumerable is to write a little code using the yield keyword. I combined these concepts to demonstrate how to quickly and easily create a custom DataSource that could implement any custom logic desired and still integrate declaratively with all existing ASP.NET controls. If you are in need of .NET training or consulting, please contact us here at Falafel Software.