While working on a legacy ASP.NET WebForms application I stumbled into the following error: I had a paged GridView with a LinkButton in a TemplateField that, when clicked, accessed the GridViews DataKeys collection. When clicking one of the LinkButtons on the first page everything worked beautifully, but as soon as a user navigated to any page other than the first clicking on the LinkButton resulted in the following exception when attempting to access the DataKeys collection: Index was out of range. Must be non-negative and less than the size of the collection.
The markup for the LinkButton was as follows:
<asp:GridView ID="gvProducts" runat="server"
<asp:LinkButton ID="lb" runat="server"
CommandArgument='<%# Container.DataItemIndex %>'
CommandName="Test" Text="Click Me"></asp:LinkButton>
Note that the LinkButtons CommandArgument property is set to the index of the current row being bound to the grid via the Container.DataItemIndex syntax.
Whenever the LinkButton is clicked there is a postback and the GridViews RowCommand event handler fires. From here I was reading the CommandArgument value to get the row index and then used that to access the DataKeys collection.
protected void gvProducts_RowCommand(object sender, GridViewCommandEventArgs e)
if (e.CommandName == "Test")
int rowIndex = Convert.ToInt32(e.CommandArgument).ToString();
object value = gvProducts.DataKeys[rowIndex].Value;
This code works great if the GridView is not paged or if viewing the first page of a paged GridView. It blows up, however, when clicking the LinkButton in a paged GridView when viewing any page other than the first. Specifically, it blows up because the value of rowIndex exceeds the bounds of the DataKeys collection.
Container.DataItemIndex returns the index of the data item whereas the DataKeys collection only contains entries for the current page of data being displayed. To put it in simple terms, if you have a paged GridView that displays 10 records per page and you bind 100 records to it, the Container.DataItemIndex value will return a value between 0 and 99 no matter what page you are viewing; the DataKeys collection, on the other hand, will only have 10 records at most (since only 10 records are being displayed per page). So if you are viewing page 2 of the GridView the Container.DataItemIndex values will range from 10..19, but the DataKeys collection is still only going to have 10 records (0..9). So when you go to page 3, say, and click the LinkButton for the first row in that page you are actually running code like:
object value = gvProducts.DataKeys.Value;
But the DataKeys collection only has 10 items (0..9). The result is a System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
There are two workarounds here. The first is to set the LinkButtons CommandArgument property to the value you actually care about. If you want to read the ProductID value for the row just send it on in (rather than sending in the row index and reading the value from the DataKeys collection), like so:
<asp:LinkButton ID="lb" CommandArgument='Eval("ProductID")' ... />
The other option is to do a little modular arithmetic in the RowCommand event handler. Taking the row index mod the number of records per page will give you the appropriate value by which to access the DataKeys collection. GridViewID.PageSize will return the number of records per page so all you need to do is do rowIndex mod pageSize, as the following code snippet shows:
object value = gvProducts.DataKeys[rowIndex % gvProducts.PageSize].Value;