
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 colums to work. I 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 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 the SqlDataSource and the 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 object also allow you to implement Insert Update and Delete. With a SqlDataSource you point the method to a database stored procedure, table or query. With an ObjectDataSource, which is what is in this example, you point the Select method to a type and a method name. Notice that these object 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. 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 GetData(string p_sortExpression, string p_sortDirection)
{
List peoples = new List();
#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. T he second is to have the first and last button 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�, �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. 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 datasource. So in the print Gridview page you just have to add the columns and set the datasource. 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 the 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 decimal point. The tip here is that for that column you must set HTMLEncode = false. Otherwise, the formatting doesn't work.
Another tip for storing the row key in the Gridview, but not displaying it. I create a template column and set its visible property = 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 the 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 peoples = (List)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 your situation calls for.