WindowsDevCenter.com
oreilly.comSafari Books Online.Conferences.

advertisement


AddThis Social Bookmark Button

Implementing Custom Data Bindable Classes: CollectionBase

by James Still
06/02/2003

Introduction

By now, everyone is familiar with grabbing a DataSet from a database and binding it at runtime to an ASP.NET list control. This works just fine. But DataSets are weakly typed, providing late bound access to their properties. What if you want to use your own strongly typed custom object with meaningful properties and methods instead of a generic DataSet? In this article, we'll do just that by developing a custom collection that implements the CollectionBase abstract base class. This base class is provided in the System.Collections namespace for just this purpose. Then we'll bind a collection of products to an ASP.NET Repeater control. Why a Repeater control? Because the "hello world" DataGrid examples are getting pretty stale.

CollectionBase

CollectionBase is an abstract base class designed to be inherited and extended to support strongly typed custom collections. It inherits three interfaces, ICollection, IEnumerable, and IList, which define interface methods that accept System.Object types as elements in the collection. The collection is implemented with an encapsulated ArrayList, which it exposes as a protected property called InnerList. This is where the collection elements are stored. So to create a custom collection, we simply inherit this abstract class and then extend it to meet our specific needs. Why not just implement against those interfaces directly rather than using CollectionBase? You could, of course, but then you'd have to override every interface method, even the ones you won't ever use. By deriving your custom collection classes from CollectionBase, you can implement just the methods that you need.

Example Project

Related Reading

Programming ASP .NET
By Jesse Liberty, Dan Hurwitz

In this example, our custom collection class is a collection of Product objects. The Product class will be an inline class encapsulated inside of the Products class. For the sake of simplicity, each Product has only one property: Name. In a new ASP.NET project, create a class called Products and declare the System.Collections namespace:


using System;
using System.Collections;

We will want to add elements to the collection, remove them, and sort all products by name in alphabetical order. To support this, we'll implement the IList.Add and IList.Remove interface methods. Later, we'll build in support for an indexer and for sorting the collection, but for now, let's see some code. Here's the Products class, derived from CollectionBase, which implements the Add and Remove methods and defines its inline Product class:


public class Products : CollectionBase 
{
  public Products() 
  {
    // default constructor
  }

  public int Add(Product product) 
  {
    return List.Add(product);
  }

  public void Remove(Product product) 
  {
    List.Remove(product);
  }

  public class Product 
  {

    private string productName;

    public Product(string Name) 
    {
      productName = Name;
    }

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

Notice that in the Products class, the implemented methods Add and Remove call another object, named List. Every CollectionBase instance has a protected property called List that returns an IList representing the entire list of elements in the collection. As I mentioned earlier, it's implemented with an internal ArrayList, which is how the CollectionBase manages its collection of elements. When you add or remove a Product, this is the object that handles it behind the scenes for you. By default, when you call List.Add, the array is dynamically resized so that the object is inserted just after the last element in the collection. When you call List.Remove, the element is removed from the array position it occupies and all of the other elements below it (if any) move up one place in the list. (Of course, this is not the case with non-contiguous collections like hashtables, but it applies in this example.)

For simple projects, implementing no other interface method except IList.Add and IList.Remove might be all you need to use the strongly typed collection. Products can be easily added to or removed from the collection. And IEnumerable.GetEnumerator() provides built-in support for using the foreach statement to iterate over the collection. But wouldn't it be nice to provide support for indexers in the class, as well?

Declaring an Indexer

An indexer is a property that permits our custom collection object to be indexed just like an array. Once we've declared an indexer, we can access a Product in the collection by its index position. The only difference between declaring an indexer and declaring other properties is that you use the this keyword, along with a parameter defining the key. For our example, we'll declare our indexer's type as a Product object. Add this indexer to the Products class:


  public Product this[int index] 
  {
    get 
    {
      return((Product)List[index]);
    }
    set 
    {
      List[index] = value;
    }
  }

Now any consumer of this class can use the indexer to reference a specific position in the collection:


products[0].Name = "Chef Anton's Cajun Seasoning";
string name = products[0].Name;

Sorting the Collection

Before we turn our attention to the web form's Repeater control, let's implement basic sorting in the custom collection. Remember when I said that CollectionBase has a protected member called InnerList, which is an instance of the ArrayList responsible for holding elements in the collection? Well an ArrayList has a Sort method that provides us with the means to implement sorting in our collection. In this example, we'll implement ArrayList.Sort to sort each product in the collection by name in ascending alphabetical order. To accomplish this, we must first add an inline class to the Products class that implements against IComparer. Our sort method can then accept this class as a parameter:


  private class AscendingNameSorter : IComparer  
  {
    public int Compare(Object x, Object y)  
    {
      Products.Product p1 = (Products.Product)x;
      IComparable ic1 = (IComparable)p1.Name;

      Products.Product p2 = (Products.Product)y;
      IComparable ic2 = (IComparable)p2.Name;

      return ic1.CompareTo(ic2);
    }
  }


The AscendingNameSorter implements the required Compare interface method, which expects two objects for comparison. After casting each object into a Product, we instantiate an IComparable cast to the Name property. The purpose of this is to define exactly what it is that is being compared between the two objects. After that, it is simply a matter of calling the CompareTo method, which returns an integer less than zero if Product p1 has a Name lower in alpha order than Product p2. The method returns an integer greater than zero if Product p1 has a Name greater in alpha order than Product p2. CompareTo returns zero if both products have exactly the same name. I don't want to get too involved in comparisons here; see this MSDN article on the IComparable interface for details on type-specific comparisons.

Now that we've built a sorter class that defines how to compare the two objects, we can implement our own Sort method. Add this method to the Products class:


public void Sort() 
{
  IComparer NameSorter = new AscendingNameSorter();
  InnerList.Sort(NameSorter);
}

Binding to the Repeater Control

A consumer of our Products class can now instantiate it, populate it with new elements, sort by name, and finally, bind it directly to a list control. In this example we're going to bind the custom collection object to an ASP.NET Repeater control. Drop a Repeater control onto a web form. Then switch to HTML view:


<asp:Repeater id="Repeater1" runat="server"></asp:Repeater>

Add an ItemTemplate and SeparatorTemplate to the control. In the ItemTemplate, create a data-binding expression to show every Product.Name in the Products collection. In the SeparatorTemplate, insert a line break:


<asp:Repeater id="Repeater1" runat="server">
  <ItemTemplate>
    <%#DataBinder.Eval(Container.DataItem, "Name")%>
  </ItemTemplate>
  <SeparatorTemplate>
    <br />
  </SeparatorTemplate>
</asp:Repeater>

The Repeater control is smart enough to get the enumerator from our Products object at runtime and iterate over each element in the collection, displaying its Name property in the ItemTemplate. (See the article "Data Binding Server Controls" on the GotDotNet web site for an introduction to ASP.NET data binding syntax and expressions.) In the web form's Page Load event, we can populate an instance of Products and then bind it to the Repeater control:


// populate collection with elements
Products.Product p1 = new Products.Product("Ikura");
Products.Product p2 = new Products.Product("Chocolade");
Products.Product p3 = new Products.Product("Tofu");
Products.Product p4 = new Products.Product("Konbu");
Products products = new Products();
products.Add(p1);
products.Add(p2);
products.Add(p3);
products.Add(p4);

// change one product name
products[1].Name = "Aniseed Syrup";

// sort the collection
products.Sort();

// bind to control
Repeater1.DataSource = products;
Repeater1.DataBind();

After building and running the project, here's our output in the web browser:

Aniseed Syrup
Ikura
Konbu
Tofu

Conclusion

In this article, we saw how easy it is to create a custom collection class derived from CollectionBase and to implement just a few of the interface methods to add, remove, and sort elements within the collection. Because our custom collection has built-in support for enumeration, it took just two lines of code to bind it to an ASP.NET list control.

James Still James Still is an experienced software developer in Portland, Oregon. He collaborated on "Visual C# .NET" (Wrox) and has immersed himself in .NET since the Beta 1 version was released.


Return to ONDotnet.com