Partager via


Data Binding to Custom Objects

I’m back!

I apologize for my lack of recent blogging, but this post should help make up for that. We’ll be discussing data binding to custom collection objects. After reading my blog on building your own data bound controls, you’ll know that I like to stress the importance of avoiding unnecessary conversions between one data format to another. It’s completely unnecessary to create a DataSet object simply to bind it to a grid control unless your data was already in the form of a DataSet in the first place (for example, retrieved through ADO.) If you’re building your own collection classes anyway, you might as well make them expose the proper interfaces to be bindable. Once you do that, you can simply set them directly to the DataSource property of any .NET control and you’re set.

So how does one make their collection class into a source for data binding? If you haven’t yet read my blog on building your own data bound controls, I suggest you stop now and read that first. It will explain how data bound controls work and how they actually access the data in objects they’re data bound to. You’ll then have a pretty good idea of how these controls work internally and what they would expect from any custom data source object.

To recap, data bound controls such as the DataGrid and GridView controls expect data sources to be Enumerable. This means they implement IEnumerable or IEnumerable<T> (in .NET 2.0.) Not only that, each item in this enumerable list is expected to support property descriptors (usually by implementing ICustomTypeDescriptor.) In the .NET framework, you can find a few examples of objects that implement ICustomTypeDescriptor such as DataRowView. One can see how this fits in, a DataTable exposes an enumerable list (the Rows object) that expose a collection of DataRowView objects which implement ICustomTypeDescriptor. When you bind to a column in this table, the grid accesses the value of each column by calling GetProperty on the type descriptor. The grid knows nothing about DataViews or ADO data types, but type descriptors allow you to look up a property of an object using a string.

So let’s get to work. First off, I’ll be using the new GridView ASP.NET control because it’s new and cool, but you can use any control that supports data binding since they all work the same. My web page will display a list of high tech companies and provide a full mailing address. So here’s my web page:

<html xmlns="https://www.w3.org/1999/xhtml" >

<body>

    <form id="form1" runat="server">

    <div>

        <center>High-Tech Company Phone Book</center><br />

        <asp:GridView runat="Server" ID="Grid" AutoGenerateColumns="false">

            <Columns>

                <asp:BoundField HeaderText="Name" DataField="Name" />

                <asp:BoundField HeaderText="Address" DataField="Address" />

                <asp:BoundField HeaderText="City" DataField="City" />

                <asp:BoundField HeaderText="State" DataField="State" />

                <asp:BoundField HeaderText="Zip" DataField="Zip" />

            </Columns>

        </asp:GridView>

    </div>

    </form>

</body>

</html>

 

I swear I’m really a brilliant web designer but for this example, I’ve created the ugliest page known to man. I have a single GridView control that has some columns defined. This is exactly what I would do if I were using a DataSet as my data source. For those interested on the new GridView control, check out this blog (I hope she gives me a link back!)

I also have a code behind for this page:

 

public partial class _Default : System.Web.UI.Page

{

    GridBinding binding;

    protected void Page_Load(object sender, EventArgs e)

    {

        binding = new GridBinding();

       

        binding.AddRow("Microsoft Corp", "One Microsoft Way", "Redmond", "WA", 92020);

        binding.AddRow("Apple", "1 Infinite Loop", "Cupertino", "CA", 95014);

        binding.AddRow("Google Inc.", "1600 Amphitheatre Parkway", "Mountain View", "CA", 94043);

        binding.AddRow("IBM Corporation", "1133 Westchester Avenue", "White Plains", "NY", 10604);

        Page.DataBind();

    }

    public override void DataBind()

    {

        Grid.DataSource = binding;

        Grid.DataBind();

        base.DataBind();

    }

}

 

My class has a member variable of type GridBinding called “binding” which is set to the DataSource property of the Grid. GridBinding is a class I’ve created that holds a collection of rows of data. The class has a public method called AddRow that allows me to add a new row, passing in address information for my phone book. As you can imagine, this data structure might be created in some other way, returned via a web service, serialized in a database somewhere, or generated on the fly through some sort of user interaction. The reason I can set this object to the DataSource property of the grid is because the object implements IEnumerable.

So let’s take a look at GridBinding. You might imagine I’ve implemented some huge collection class with various methods to work with the data, but you’ve underestimated by laziness. I’ll just be storing the data using a private List<> variable and implementing IEnumerable<>.

 

public class GridBinding : IEnumerable<GridRow>

{

    private PropertyDescriptorCollection _propDescCol;

    private List<GridRow> _rows;

    public GridBinding()

    {

        _rows = new List<GridRow>();

        _propDescCol = new PropertyDescriptorCollection(new GridPropertyDescriptor[0]);

        _propDescCol.Add(new GridPropertyDescriptor("NAME"));

        _propDescCol.Add(new GridPropertyDescriptor("ADDRESS"));

        _propDescCol.Add(new GridPropertyDescriptor("CITY"));

        _propDescCol.Add(new GridPropertyDescriptor("STATE"));

        _propDescCol.Add(new GridPropertyDescriptor("ZIP"));

    }

    public void AddRow(string name, string address, string city, string state, int zip)

    {

        GridRow row = new GridRow(this, name, address, city, state, zip);

        _rows.Add(row);

    }

   public PropertyDescriptorCollection GetPropertyDescriptorCollection()

    {

       return _propDescCol;

    }

    IEnumerator<GridRow> IEnumerable<GridRow>.GetEnumerator()

    {

        return _rows.GetEnumerator();

    }

    IEnumerator IEnumerable.GetEnumerator()

    {

        return _rows.GetEnumerator();

    }

}

 

So let’s walk through this. First, I have a PropertyDescriptorCollection called _propDescCol. This is a collection of PropertyDescriptor objects. A PropertyDescriptor object is an object that knows how to get a specific property on a certain type of object. If you have a DataRowView object, this object contains a PropertyDescriptor for each column in your row, and the PropertyDescriptorCollection allows you to find the PropertyDescriptor you need when you try to access a property on this row. I could have a PropertyDescriptorCollection in each of my rows, but since they’re all the same, I’ve moved this into the GridBinding class for efficiency (so they’ll only be created one time.)

Next, I have a List<> of GridRows. My GridRow class allows me to store a specific row of information. I can add as many rows as I want using the AddRow method.

In the constructor, I create a new PropertyDescriptorCollection and create a PropertyDescriptor for each property that I want to allow users to bind to. I pass in the name of the property on the constructor, and this is what you’ll use when you’re binding to that property in the grid control.

I’ve also added a method called GetPropertyDescriptorCollection() that allows the GridRow object to retrieve the PropertyDescriptorCollection object.

Lastly, to fulfill the requirements of the IEnumerable<> interface, I’ve implemented GetEnumerable as both a normal and generic method. This simply returns the IEnumerator from my List<> class since I’m using this to store all my rows.

Pretty simple so far, right?

Next, let’s look at the GridRow object:

 

public class GridRow : ICustomTypeDescriptor

{

    private GridBinding _binding;

    private string _name;

    private string _address;

    private string _city;

    private string _state;

    private int _zip;

    public string Name { get { return _name; } }

    public string Address { get { return _address; } }

    public string City { get { return _city; } }

    public string State { get { return _state; } }

    public int Zip { get { return _zip; } }

    public GridRow(GridBinding binding, string name, string address, string city, string state, int zip)

    {

        _binding = binding;

        _name = name;

      _address = address;

        _city = city;

        _state = state;

        _zip = zip;

    }

    System.ComponentModel.AttributeCollection ICustomTypeDescriptor.GetAttributes()

    {

        return new System.ComponentModel.AttributeCollection(null);

   }

    string ICustomTypeDescriptor.GetClassName()

    {

        return null;

    }

    string ICustomTypeDescriptor.GetComponentName()

    {

        return null;

    }

    TypeConverter ICustomTypeDescriptor.GetConverter()

    {

        return null;

  }

    EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()

    {

        return null;

    }

    PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()

    {

        return null;

    }

    object ICustomTypeDescriptor.GetEditor(Type editorBaseType)

    {

        return null;

    }

    EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)

    {

        return new EventDescriptorCollection(null);

    }

    EventDescriptorCollection ICustomTypeDescriptor.GetEvents()

    {

        return new EventDescriptorCollection(null);

    }

    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)

    {

        return _binding.GetPropertyDescriptorCollection();

    }

    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()

    {

        return ((ICustomTypeDescriptor)this).GetProperties(null);

    }

    object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)

    {

        return this;

    }

}

 

This is a bunch of code, but you can ignore most of it. ICustomTypeDescriptor supports all sorts of stuff that most of the data bound controls in .NET don’t take advantage of. The first part is easy – I have properties for name, address, city, state, and zip. You must set all of these using the constructor.

Now, let’s go over the ICustomTypeDescriptor methods that we’ll be using here. The most important one is GetProperties(). GetProperties allows you to retrieve PropertyDescriptor objects for all properties or even a specific set of properties. These descriptors are returned back to you in the form of a PropertyDescriptorCollection object. My implementation returns all properties regardless of what you pass in, and as you can see, just calls GetPropertyDescriptorCollection on the GridBinding object. When the GridView control is data binding, it will call GetProperties() and then call Find Property() to seek the PropertyDescriptor for the property that you’re attempting to bind to.

So, in our case, what does GetPropertyDescriptorCollection return? Well, it doesn’t return a PropertyDescriptor object (since this class is abstract), but instead returns a collection of GridPropertyDescriptor objects. This is a class I’ve created to describe a specific property on a GridRow object. In the constructor for GridBinding, we created one of these objects for each property the GridRow class exposes. To my knowledge, there’s no property descriptors built in to .NET, and the ones used internally (such as the one used in DataRowView) are marked as internal. So let’s take a look at the code for GridPropertyDescriptor.

 

public class GridPropertyDescriptor : PropertyDescriptor

{

    private string _propName;

    public GridPropertyDescriptor(string propname) : base(propname, null)

    {

        _propName = propname.ToUpper();

    }

    public override bool CanResetValue(object component)

    {

        return false;

    }

    public override Type ComponentType

    {

        get { return null; }

    }

    public override object GetValue(object component)

    {

        GridRow row = component as GridRow;

        if (row != null)

        {

            switch (_propName)

            {

                case "NAME":

                    return row.Name;

                case "ADDRESS":

                    return row.Address;

                case "CITY":

    return row.City;

                case "STATE":

                    return row.State;

                case "ZIP":

                    return row.Zip;

            }

        }

        return null;

    }

    public override bool IsReadOnly

  {

        get

        {

            return true;

        }

    }

    public override Type PropertyType

    {

        get

        {

            return null;

        }

    }

    public override void ResetValue(object component)

    {

    }

    public override void SetValue(object component, object value)

    {

        //TODO: Set value on component the same way GetValue works...

    }

    public override bool ShouldSerializeValue(object component)

    {

        return false;

    }

}

 

Again, most of this code can be ignored. Let’s go through each method.

First, there’s a private string called _propName. This stores which property of the GridRow this descriptor will be responsible for accessing. This is to be set on the constructor.

The main method here is GetValue. You pass in an object of type GridRow (it would be cool if PropertyDescriptor was a generic class, but it’s not so we have to cast.) Depending on the value of _propName, we return a different property on the GridRow object. If you pass in an object that isn’t of type GridRow, it would return null. Also, if you created a GridPropertyDescriptor with an invalid property name, this would also return null. Perhaps it would be best to check for this, or maybe use an enum for the property name. However, this is just an example so it’s not too important.

The other methods are pretty self-explanatory. Most of them focus on providing information about the property – what type of data it is, if you can change it, if you can serialize it, if you can reset it back to its original value, etc. The method you would need to implement if you wanted to support editing in your grid is, of course, the SetValue method. This method takes a component and a new value. To implement this, you would do the same thing as GetValue only set the value of the property in the GridRow class instead of just return it.

That’s pretty much it! When I run my page, I get the following output:

High-Tech Company Phone Book

Name

Address

City

State

Zip

Microsoft Corp

One Microsoft Way

Redmond

WA

92020

Apple

1 Infinite Loop

Cupertino

CA

95014

Google Inc.

1600 Amphitheatre Parkway

Mountain View

CA

94043

IBM Corporation

1133 Westchester Avenue

White Plains

NY

10604

It’s pretty ugly, I know, but this isn’t an article on how to create pretty grids – there’s already enough of those. Hopefully this gives you a good deal of insight about how data bound controls work and how classes such as DataRowView and Hashtable implement property descriptors to abstract property access. So next time you want to new up a DataSet just to bind to a control, think about writing your own collection class that supports being bound to a data bound control.

Enjoy!

Mike – Web Dev Guy

PS – This blog was posted using the new blog publishing feature in Word 2007, pretty cool!

Comments

  • Anonymous
    May 18, 2006
    Hi,
    I need to read this article again. From what I understand, you are demonstrating how to bind to custom objects. Why didn't you use a List<Company>
    where Company is
    public class Company {
    public string Name {...}
    public string Address {...}
    public string City {...}
    public string State{...}
    public string Zip{...}
    }

    and just bind to that list? Did you do it this way to demonstrate how PropertyDescriptors work?

    Thanks.

  • Anonymous
    May 18, 2006
    My previous comment did not make it through.

    Why did you not simply use a List<Company> to bind to?

    Thanks.

  • Anonymous
    May 30, 2006
    The other day, one of my readers posed a question regarding my recent post “Data Binding to Custom Objects”...

  • Anonymous
    May 31, 2006
    Thanks for the comments Ron, yea I got this question from a few different people.  I just posted an article explaining how to use the "default property descriptors" so you don't need to write all your own code.  You're correct, my article explains how property descriptors work internally, but the example was probably oversimplified and there's easier ways to do that.  Thanks!

    Mike

  • Anonymous
    September 29, 2006
    Creating large flat classes for data binding is one way of doing things. But you should check out this...

  • Anonymous
    October 17, 2006
    Check below article written by Mike http://blogs.msdn.com/mikechr/archive/2006/05/17/600697.aspx

  • Anonymous
    December 03, 2006
    Great one. I'm working on a similar functionality. But I have included textboxes in the GridView(ItemTemplates). Need to provide an update option to it. Please help me how to do a Reverse DataBinding. I hope I'm clear on this. Many Thanks, Vinay