Flexible Data Binding

Generic ResolveDataSource Can Bind to Any Data Source That a DataGrid Can Bind To

AskthePRO

LANGUAGES: C#

ASP.NET VERSIONS: 1.0 | 1.1

 

Flexible Data Binding

Generic ResolveDataSource Can Bind to Any Data Source That a DataGrid Can Bind To

 

By Jeff Prosise

 

Q. How do I build data-binding controls that, like ASP.NET's DataGrid control, are equally capable of binding to simple data sources such as ArrayLists and complex data sources such as DataSets?

 

A. As you've probably discovered, there's more to it than meets the eye. Binding to data sources that directly expose an IEnumerable interface is straightforward; in the control's DataBind method, you call IEnumerable.GetEnumerator to acquire an IEnumerator interface, and then use IEnumerator methods to enumerate items. Binding to DataSets and other complex data sources is more problematic, because you have to reach inside the data source and find a subitem that implements IEnumerable.

 

To demonstrate flexible data binding, I've built a sample control that I call BulletList. It displays bulleted lists of items, and is capable of binding to any type of data source that a DataGrid can bind to. Furthermore, except for the fact that it doesn't expose its contents through an Items collection, it's similar to the BulletedList control coming in ASP.NET 2.0. You can download the source code for the completed control (see end of article for details). Key portions of that source code are reproduced here.

 

The first requirement for building a data-binding control is to implement a DataSource property that stores a reference to the data source. Figure 1 contains BulletList's DataSource implementation. The property's set accessor accepts interfaces of type IEnumerable and IListSource. It also accepts null, since setting a data-binding control's DataSource property to null is considered a valid operation. "Simple" data sources such as ArrayList and SqlDataReader implement IEnumerable or IEnumerable-derivatives; more complex data sources such as DataSets implement IListSource.

 

public object DataSource

{

   get

  {

     return _dataSource;

  }

   set

  {

     if (value == null ||

         value as IListSource != null ||

         value as IEnumerable != null)

      _dataSource = value;

     else

       throw new ArgumentException("Invalid data source");

  }

}

Figure 1: BulletList's DataSource property accepts IEnumerable and IListSource interfaces.

 

The second requirement for building a data-binding control is to override the virtual DataBind method, and enumerate the items in the data source in the override. Figure 2 shows BulletList's DataBind implementation. After calling the base class and deleting any existing items, DataBind calls a local method named ResolveDataSource to extract an IEnumerator interface from the data source. Once it has the interface, DataBind uses IEnumerator.MoveNext to enumerate items from the data source and store them in the control's own _items field.

 

public override void DataBind()

{

   base.DataBind();

  _items.Clear();

   if (_dataSource != null)

  {

    IEnumerator e =

      ResolveDataSource(_dataSource, _dataMember);

     if (e != null)

    {

       while (e.MoveNext())

      {       

         string item;

         if (_dataTextField != null)

          item = DataBinder.Eval(

            e.Current, _dataTextField).ToString();

         else

          item = e.Current.ToString();

        _items.Add(item);

      }

    }

  }

}

Figure 2: BulletList's DataBind method uses ResolveDataSource to convert a data source reference into an IEnumerator interface.

 

The heart of the BulletList control - the part that does the hard work of converting an arbitrary data source into an IEnumerator interface - is the ResolveDataSource method (see Figure 3). ResolveDataSource first checks to see if the data source implements IEnumerable. If the answer is yes, ResolveDataSource calls IEnumerable.GetEnumerator to acquire a reference to an IEnumerator interface. This simple logic enables the control to support data sources that implement IEnumerable or IEnumerable-derivatives such as ICollection and IList.

 

If the data source exposes an IListSource interface, rather than an IEnumerable interface, then ResolveDataSource must dig deeper. Suppose the data source is a DataSet. DataSets implement IListSource; so do the DataTables they contain. Ultimately, a DataSet used as a binding target must be resolved to a DataView, which implements IEnumerable. That's the reason for all that code in ResolveDataSource. Sure, ResolveDataSource could just do something like this:

 

if (source is DataSet)

   return ((IEnumerable)source.Tables[

             member].DefaultView).GetEnumerator();

 

But then you'd have to add similar logic for binding to DataTables, and such type-specific logic rules out binding to future types of data sources that are as yet undefined.

 

IEnumerator ResolveDataSource(object source, string member)

{

   if (source is IEnumerable)

     return ((IEnumerable)source).GetEnumerator();

   else if (_dataSource is IListSource)

  {

    IListSource ls = (IListSource)source;

     if (!ls.ContainsListCollection)

       return ls.GetList().GetEnumerator();

     else

    {

      IList list = ls.GetList();

       if (list != null && list is ITypedList)

      {

        ITypedList tl = (ITypedList)list;

        PropertyDescriptorCollection pdc =

          tl.GetItemProperties(new PropertyDescriptor[0]);

         if (pdc != null && pdc.Count != 0)

        {

          PropertyDescriptor pd = null;

           if (member == null)

            pd = pdc[0];

           else

            pd = pdc.Find(member, true);

           if (pd != null)

          {

            object e = pd.GetValue(list[0]);

             if (e != null && e is IEnumerable)

               return ((IEnumerable) e).GetEnumerator();

          }

           throw new ArgumentException("\"" + member +

                      "\" not found in data source");

        }

         throw new ArgumentException("Empty data source");

      }

    }

  }

   return null;

}

Figure 3: ResolveDataSource converts an IEnumerable or IListSource into an IEnumerator.

 

Incidentally, the first time I wrote a control that does complex data binding, it wasn't at all obvious to me how to handle a variety of data sources, nor how to do so in a generic, non-type-specific way. So I looked to the .NET Framework Class Library for help. System.Web.dll contains an internal class named DataSourceHelper. Inside DataSourceHelper is a method named GetResolvedDataSource that takes a data source as input and returns an IEnumerable interface.

 

GetResolvedDataSource is used by virtually all ASP.NET data-binding controls. Figure 3's ResolveDataSource method is patterned after GetResolvedDataSource. The chief difference is that ResolveDataSource performs the extra step of resolving an IEnumerable interface into an IEnumerator. That's one less line of code for your DataBind method.

 

You can try BulletList for yourself by building a simple page to host it. Try binding to an ArrayList, a DataReader, a DataSet, even a DataTable or DataView, and the control should work equally well regardless of the data source type.

 

The sample code in this article is available for download.

 

Jeff Prosise is the author of several books, including Programming Microsoft .NET (Microsoft Press, 2002). He's also a cofounder of Wintellect (http://www.wintellect.com), a software consulting and education firm that specializes in .NET. Have a question for this column? Submit queries to [email protected].

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish