In working on my upcoming Data Tutorials for MSDN, alert coder Amrinder S. wrote in voicing the following concern:
I have been waiting for tutorials showing how to do custom paging with the GridView. So up until now, I have been using the old DataGrid.
One thing that I noticed when I was working with the code is an issue with deleting and paging. Say for example, the GridView has 12 rows and Page Size is set to 10. If the user goes to Page 2 and deletes the two rows, the Grid will disappear from the page. After searching around I found the solution. An Deleted event handler has to be added to the ObjectDataSource control and the following line has to be added: e.AffectedRows = (int)e.ReturnValue;
The problem Amrinder unearthed has to do when implementing custom paging in a GridView using an ObjectDataSource that also supports deleting. The GridView provides two flavors of paging - custom paging and default paging. Default paging is the default paging implementation model and can be implemented by simply setting the GridView's AllowPaging property to True. While default paging is a breeze to implement, it's very inefficient since for each page of data being viewed, all of the database records are returned (even though only a potentially small subset of them are displayed to the end user). Custom paging solves this inefficiency, but requires that the page developer be able to grab the precise subset of records to display. See Custom Paging in ASP.NET 2.0 with SQL Server 2005 for more information and details on the differences between default and custom paging and for specifics on implementing custom paging...
As Amrinder points out, when deleting the last record from the last page of a GridView using custom paging, the GridView disappears, even if there are additional records in preceding pages. (This is not a problem with default paging however, as deleting the last record from the last page returns leaves you at the new last page.) This custom paging / deleting the last record in the last page thing can be fixed by simply creating a Deleted event handler for the ObjectDataSource and manually setting e.AffectedRows to some value greater than 0 (assuming the delete operation succeeded).
What in the world is the issue here?
I wondered the same thing myself and decided to find out exactly why this oddity occurs. Firing up Reflector I started digging through System.Web.... and here's what I found!
When the end user clicks the Delete button for a row, a postback occurs and the following sequence of steps transpire:
The GridView tells its ObjectDataSource to go ahead and delete the record whose Delete button was clicked
The ObjectDataSource invokes the Delete method
The ObjectDataSource returns information about the operation just performed via an ObjectDataSourceEventArgs
instance. This class contains an AffectedRows
property that indicates how many records were affected by the operation. However, the ObjectDataSource does not
set the AffectedRows
property, though, meaning that a value of -1
is returned for this property
The GridView runs a method after the delete completes. In this post-action method, it does two important things: first, if the AffectedRows
value from the delete operation is greater than 0 it checks to see if the record deleted was the last record on the page. If so, it decrements the GridView's PageIndex property
; regardless of the AffectedRows
value, this post-action method sets the RequiresDataBinding property
to True, which means that the data will be rebound to the GridView in the PreRender stage of the page lifecycle.
In the PreRender stage, when it's determined that the GridView needs to be rebound, the data is re-retrieved from the data source
The GridView is then rebound to the data. In its CreateChildControls method, the GridView checks to see if its PageIndex property is too large (i.e., if it's greater than the total number of pages provided by the data source). If so, the PageIndex property is decremented and then the data retrieved in Step 3 is bound to the grid.
The reason the GridView disappears after deleting the last record of the last page when using custom paging is because the ObjectDataSource fails to indicate that any rows were affected. Therefore, the GridView's PageIndex property is not updated until Step 4. That means that in Step 3, when the GridView re-retrieves the data, it's asking for the page of data whose last record was just deleted in Step 1! Namely, no data is coming back. Therefore, even though the PageIndex is decremented in Step 4, it's too late, as the data being bound to the GridView has already been retrieved and its empty.
To fix this problem we need to make sure that the PageIndex is updated BEFORE Step 3. This can be accomplished using either one of the following two approaches:
- Manually set the e.AffectedRows property in the ObjectDataSource's Deleted event handler. Of course, only do so if the delete operation succeeded.
- In the GridView's RowDeleted event handler, update the GridView's PageIndex if the delete occurred for a page of data that has only one record (namely, we just deleted the last record of the page). Again, you want to only do this if the delete actually succeeded. This can be accomplished using the following code:
protected void GridView1_RowDeleted(object sender, GridViewDeletedEventArgs e)
// If we just deleted the last row in the GridView, decrement the PageIndex
if (e.Exception == null && GridView1.Rows.Count == 1)
// we just deleted the last row
GridView1.PageIndex = Math.Max(0, GridView1.PageIndex - 1);
This latter technique is similar to the technique that must be used in ASP.NET 1.x when deleting from a pageable DataGrid...
It's unfortunate that this bug made its way out into production, but so it goes. At least there's a not-too-painful workaround. And the lesson to take away from this blog entry - Reflector is your friend. :-)