The GridView control is one of the great time savers of ASP.NET. Sorting, paging, editing, deleting, and even AJAX (set EnableSortingAndPagingCallbacks to True) is included out of the box. The ObjectDataSource is the other half of the magic, connecting the GridView to the data and managing any filter controls. When using DataSets and TableAdapters, everything is a dream, but when using your own custom objects the wizards and property pages act like you don't exist. The good news is with a little extra code, we can have our custom objects act just like fancy DataSets.
First thing you'll need is a class to represent a row in the GridView. For the GridView to show the fields in the "Edit Fields..." design time dialog, you'll need to expose the fields as Properties. A very simple class for a Person could look like this:
using System;
using System.ComponentModel;
public class Person: IEquatable<Person> {
private String _name;
[DataObjectField(true)]
public String Name {
get { return _name; }
set { _name = value; }
}
private String _title;
[DataObjectField(false)]
public String Title {
get { return _title; }
set { _title = value; }
}
public Person() { }
public Person(String Name, String Title) {
this.Name = Name;
this.Title = Title;
}
#region IEquatable<Person> Members
public bool Equals(Person other) {
return this.Name.Equals(other.Name);
}
#endregion
}
If you've not seen the [ ] syntax before, these are Attributes. This simple markup tells the compiler additional information about the entity, which is then used to generate additional code. A very common Attribute is Serializable, which means the object can be saved. A class marked [Serializable] would have code added to save and load a saved object automatically. In our case we are using System.ComponentModel.DataObjectField to markup the field for the GridView. The true parameter marks the field as a primary key, which will automatically add the field name to the "DataKeyNames" list of the GridView. Other options you can add are isIdentity, isNullable, and length. Note, this just describes the field, it's up to you to enforce these constraints.
The IEquatable<> part isn't something for the GridView, but to help us manage our custom object. This Interface has one method, Equals, which is used when comparing one object to another to determine if they are the same. In our case, we are going to assume they are equal if the have the same Name (our Primary Key).
Now let us look at our adapter class:
using System;
using System.Web;
using System.Collections.Generic;
using System.ComponentModel;
public class PersonList : List<Person> { };
[DataObject(true)]
public class PersonAdapter {
private PersonList _data {
get {
if (HttpContext.Current.Session["_data"] == null) return new PersonList();
else return (PersonList)HttpContext.Current.Session["_data"];
}
set { HttpContext.Current.Session["_data"] = value; }
}
[DataObjectMethod(DataObjectMethodType.Select, true)]
public PersonList GetAll() {
return _data;
}
[DataObjectMethod(DataObjectMethodType.Delete, true)]
public void DeletePerson(Person person) {
PersonList people = _data;
people.Remove(person);
_data = people;
}
[DataObjectMethod(DataObjectMethodType.Update, true)]
public void UpdatePerson(Person person) {
PersonList people = _data;
people.Remove(person);
people.Add(person);
_data = people;
}
[DataObjectMethod(DataObjectMethodType.Insert, true)]
public void InsertPerson(Person person) {
PersonList people = _data;
people.Add(person);
_data = people;
}
}
First thing to notice is the use of Generics - we are using a List<> to represent a set of Person objects. Inheriting from List<> in this manner buys us some ease in data management, but the List<> class already supports paging by controls such as the GridView. Since it's subclassed, our users won't need to know anything about Generics to use the adapter. The reason we implemented IEquatable above is to support the List.Remove method, used in updates and deletes.
Like before, we use some Attributes to let the Visual Studio designer know about our class. First, we mark the class with System.ComponentModel.DataObject(true) - this tells Visual Studio to list the class in the Choose a Business Object screen of the configure datasource wizard. System.ComponentModel.DataObjectMethod above our methods tell the wizard what type of method (Select, Delete, Edit, or Insert) and if it's the default method for that type.
If you want to see all this in action, create a website with the two classes above, and then a form with the following controls:
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
DataObjectTypeName="Person" TypeName="PersonAdapter"
DeleteMethod="DeletePerson" InsertMethod="InsertPerson"
SelectMethod="GetAll" UpdateMethod="UpdatePerson" />
<asp:GridView ID="GridView1" runat="server" AllowPaging="True"
DataKeyNames="Name" DataSourceID="ObjectDataSource1"
AutoGenerateDeleteButton="True" AutoGenerateEditButton="True"
AutoGenerateColumns="True" />
<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False"
DataKeyNames="Name" DataSourceID="ObjectDataSource1"
DefaultMode="Insert" AutoGenerateInsertButton="True" >
<Fields>
<asp:BoundField DataField="Title" HeaderText="Title"/>
<asp:BoundField DataField="Name" HeaderText="Name" />
</Fields>
</asp:DetailsView>
As you can see, using the methods above we need very little on our form. In a future article I'll take things further, including enabling sorting on our custom objects.