
Introduction
This article attempts to show a few tricks with the new GridView
that we have in ASP.NET 2.0. This article addresses using the ObjectDataSource
to populate the GridView
. In this example, the ObjectDataSource
returns a generic collection of an object. There is some special coding that needs to happen to get the sorting of columns to work. I will also show how to use the pager template to do custom navigation. I have also included a simple GridView
printing example.
Background
I have been meaning to write an article on the GridView
for sometime. I really like the improvements .NET 2.0 has over the DataGrid
. Using a GridView
in combination with a SqlDataSource
or ObjectDataSource
will give you a lot of functionality without having to write a lot of code.
The Code
First, let's take a look at the People
class I put together. Notice that I just have a few properties on it. I just needed something simple for this example, so this is what I came up with.

Next, let's talk about ObjectDataSource
. Both SqlDataSource
and ObjectDataSource
give you a lot of functionality when you point your GridView
to one of these objects for data. I have only implemented the Select method for this example, but these objects also allow you to implement Insert, Update, and Delete. With SqlDataSource
, you point the method to a database Stored Procedure, table, or query. With an ObjectDataSource
, which is what is used in this example, you point the Select method to a type and a method name. Notice that these objects can implement caching. Here's the HTML:
<asp:ObjectDataSource id=ObjectDataSource1 CacheExpirationPolicy="Sliding"
CacheDuration="300" TypeName="DataAccess"
SelectMethod="GetData" runat="server">
<SELECTPARAMETERS>
<asp:Parameter Direction="input" Type="string" Name="p_sortExpression">
</asp:Parameter>
<asp:Parameter Direction="input" Type="string" Name="p_sortDirection">
</asp:Parameter>
</SELECTPARAMETERS>
</asp:ObjectDataSource>
Once the ObjectDataSource
is set up, you just need to point the GridView
DataSourceID
equal to the ObjectDataSource
name. I am going to have my ObjectDataSource
method (GetData
) return a List<PEOPLE>
. This generic collection has one drawback. The sorting doesn't work automatically, like it does if you return a DataSet
, DataTable
, or DataView
from an ObjectDataSource
. If you try sorting, you will actually get this exception:
The data source 'ObjectDataSource1' does not support sorting with
IEnumerable data. Automatic sorting is only supported with
DataView, DataTable, and DataSet.
So the way I got around this exception is I created an event handler for the GridView.Sorting
event. In this event, I set the ObjectDataSource
SortExpression
and SortDirection
parameters to the values I get from the GridView
from the sorting event. Finally, I cancel the sorting event so I don't get the exception. Here is the event code:
protected void GridView1_Sorting(object sender, GridViewSortEventArgs e)
{
if (ObjectDataSource1.SelectParameters[0].DefaultValue == null ||
ObjectDataSource1.SelectParameters[1].DefaultValue == null ||
ObjectDataSource1.SelectParameters[0].DefaultValue !=
e.SortExpression ||
ObjectDataSource1.SelectParameters[1].DefaultValue == "Desc")
{
ObjectDataSource1.SelectParameters[1].DefaultValue = "Asce";
}
else if (ObjectDataSource1.SelectParameters[1].DefaultValue == "Asce")
{
ObjectDataSource1.SelectParameters[1].DefaultValue = "Desc";
}
ObjectDataSource1.SelectParameters[0].DefaultValue = e.SortExpression;
e.Cancel = true;
}
The next thing I need is to implement the ObjectDataSource
Select method. It looks like this:
public static List<PEOPLE> GetData(string p_sortExpression, string p_sortDirection)
{
List<PEOPLE> peoples = new List<PEOPLE>();
#region Creating data
peoples.Add(new People(1, "Bart", "Long", "Mower", 10.00M, 18));
...
peoples.Add(new People(21, "Gary", "Black", "Doctor", 46.50M, 29));
#endregion
if (p_sortExpression != null && p_sortExpression != string.Empty)
{
peoples.Sort(new PeopleComparer(p_sortExpression));
}
if (p_sortDirection != null && p_sortDirection == "Desc")
{
peoples.Reverse();
}
return peoples;
}
Next, we need to look at the PeopleComparer
that is used to sort the generic collection. I use Reflection so that I can just find the property that is passed into the comparer. Here is the code:
public int Compare(People x, People y)
{
Type t = x.GetType();
PropertyInfo val = t.GetProperty(this.PropertyName);
if (val != null)
{
return Comparer.DefaultInvariant.Compare(val.GetValue(x,null),
val.GetValue(y,null));
}
else
{
throw new Exception(this.PropertyName +
" is not a valid property to sort on. " +
"It doesn't exist in the Class.");
}
}
Let's move on to navigation. I really like the pager functionality they have added to the GridView
. There are still a few things I would like my navigation to do. First is to have a label that tells me which page I am on out of how many pages. The second is to have the first and last buttons disappear when you are on the first and last pages, respectively. Finally, it is also nice to have a dropdown that lets the user choose how many items per page are shown. The thing to note about doing your own navigation in a pager template is that you just need to mark the controls with the correct CommandName
, which should be "Page", and the correct CommandArgument
, which will be "First", "Prev", "Next", or "Last". If you implement these two things, paging will work as long as you have a SqlDataSource
or a ObjectDataSource
behind the scenes. This is where it is nice to implement some sort of caching on these objects so you don't hit your Select method every time you navigate your GridView
. Here's the HTML for the pager template:
<PagerTemplate>
<table id="tbPager" width="100%">
<tr>
<td>Page <asp:Label ID="lbCurrentPage" runat="server"></asp:Label> of
<asp:Label ID="lbTotalPages" runat="server"></asp:Label>
</td>
<td>
<asp:DropDownList ID="ddlPageItems" runat="server" AutoPostBack="True"
OnSelectedIndexChanged="ddlPageItems_SelectedIndexChanged">
<asp:ListItem>5</asp:ListItem>
<asp:ListItem Selected="True">10</asp:ListItem>
<asp:ListItem>20</asp:ListItem>
</asp:DropDownList></td>
<td align="right">
<asp:LinkButton ID="lbtnFirst" runat="server" CommandName="Page"
CommandArgument="First" Text="<<"></asp:LinkButton>
<asp:LinkButton ID="lbtnPrev" runat="server" CommandName="Page"
CommandArgument="Prev" Text="Prev"></asp:LinkButton>
<asp:LinkButton ID="lbtnNext" runat="server" CommandName="Page"
CommandArgument="Next" Text="Next"></asp:LinkButton>
<asp:LinkButton ID="lbtnLast" runat="server" CommandName="Page"
CommandArgument ="Last" Text=">>"></asp:LinkButton>
</td>
</tr>
</table>
</PagerTemplate>
Next, I write a handler for the GridView.PreRender
event. This is where I show or hide the first and last navigation buttons and where I set the current page and total pages. There is one thing to note about the pager template. Since the pager template can be both a top and bottom pager control, you have to check both if you are using them. You will find the control resolves to TopPagerRow
and BottomPagerRow
off of the GridView
control. In this example, I only use the TopPagerRow
. I have found that when using both top and bottom, some of the control events don't work properly. Like in the case of the dropdownlist event for the number of items on a page. It seems to work the first time on the BottomPagerRow
, but doesn't fire after that. Anyway, here is the pre-render event for the GridView
:
protected void GridView1_PreRender(object sender, EventArgs e)
{
if (GridView1.TopPagerRow != null)
{
((Label)GridView1.TopPagerRow.FindControl("lbCurrentPage")).Text =
(GridView1.PageIndex + 1).ToString();
((Label)GridView1.TopPagerRow.FindControl("lbTotalPages")).Text =
GridView1.PageCount.ToString();
((LinkButton)GridView1.TopPagerRow.FindControl("lbtnFirst")).Visible =
GridView1.PageIndex != 0;
((LinkButton)GridView1.TopPagerRow.FindControl("lbtnLast")).Visible =
GridView1.PageCount != (GridView1.PageIndex +1);
DropDownList ddlist =
(DropDownList)GridView1.TopPagerRow.FindControl("ddlPageItems");
ddlist.SelectedIndex =
ddlist.Items.IndexOf(ddlist.Items.FindByValue(
ViewState["DropDownPageItems"].ToString()));
}
}
I also have an example of simple GridView
printing. The idea is to put the columns of the GridView
you want to print into a session variable. Then, in another session variable, you put the data source. So in the Print GridView page, you just have to add the columns and set the data source. Here is the Print button method:
protected void btnPrint_Click(object sender, EventArgs e)
{
Session["PrintGridviewColumns"] = this.GridView1.Columns;
Session["PrintGridViewDataSource"] = this.ObjectDataSource1.Select();
StringBuilder sb = new StringBuilder();
sb.Append("<script>");
sb.Append(Environment.NewLine);
sb.Append("window.open(\"PrintGridview.aspx\",\"Print\",\"top=5,left=5\");");
sb.Append(Environment.NewLine);
sb.Append("</script>");
ClientScript.RegisterStartupScript(this.GetType(), "print", sb.ToString());
}
Here is the page load for PrintGridView.aspx:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack && Session["PrintGridviewColumns"] != null &&
Session["PrintGridViewDataSource"] != null)
{
foreach (DataControlField dcf in (DataControlFieldCollection)
Session["PrintGridviewColumns"])
{
this.GridView1.Columns.Add(dcf);
}
this.GridView1.DataSource = Session["PrintGridViewDataSource"];
this.GridView1.DataBind();
StringBuilder sb = new StringBuilder();
sb.Append("<script language="javascript">");
sb.Append("window.print();");
sb.Append("</script>");
ClientScript.RegisterStartupScript(this.GetType(),"print", sb.ToString());
Session["PrintGridviewColumns"] = null;
Session["PrintGridViewDataSource"] = null;
}
}
Another tip has to do with the DataFormatString
in a bound column. In my example, the DollarsPerHour
column is formatted to have a dollar sign and a decimal point. The tip here is that for that column, you must set HTMLEncode = false
. Otherwise, the formatting doesn't work.
Another tip is for storing the row key in the GridView
, but not displaying it. I create a template column and set its Visible
property to false
. Then in the item and edit template, I have a label that is bound to the key name. This way, I can access the key later on.
foreach (GridViewRow tmpGVR in p_gridview.Rows)
{
tmpLBKey = (Label)tmpGVR.FindControl("lbKey");
}
One last thing on the GridView printing example. If you have template columns, you must use DataBinder.Eval(Container.DataItem, "Key")
verses the newer Bind("Key")
. For some reason, when you pass the columns from your GridView
to the printing page, it won't bind right to the Bind("Key")
syntax.
Finally, I just added this in since I thought it was a good thing to know. If you want to search a generic collection and find a certain object in the collection based off a property in the object, here is one way of doing it. In this example, you pass in the key value and get back a People
object if it can be found in the generic collection.
private People FindByID(int p_key)
{
List<PEOPLE> peoples = (List<PEOPLE>)this.ObjectDataSource1.Select();
return peoples.Find(delegate(People p) {return p.Key == p_key;});
}
In this code, you see an example of an anonymous method delegate.
Conclusion
Well, I hope you have learned some new things about the GridView
and some tips to help you make your implementation of the GridView
work in whatever way your situation calls for.