Tuesday, 26 February 2008

Persisting Selection in ASP.NET Grid Controls While Paging

By: Mike Pope

How to persist selection correctly in a grid control (DataGrid, GridView, and ListView controls) when paging is enabled.

Introduction
I only recently ran across this, and it surprised me: when you use a grid control (DataGrid, GridView, and now even ListView) that supports both selection and paging, selection is persisted incorrectly as you navigate between grid pages.
Let's say that you select the second row. Use the pager to go to page two. You'll see that the second row on that page is selected, too. Third page, same thing. Turns out that row selection in a grid control is relative to the page - the default selection mechanism just selects "the second row" no matter what page you're on. I investigated a bit - surely this can't be what was intended? - but as far as I can determine, this behavior is by design.
I was surprised to discover this behavior at all, and I was surprised that I'd never ran across it at all, and surprised that a few seconds of Web search didn't turn up a solution. So I set about fixing it for my application. It initially turned out to be more complicated than I thought. I haven't run across an easier way yet, although I'm still suspicious that there might be.
For the fix, I handled two events: the selection event that's raised when you click the Select button in a grid row and the paging event that's raised in response to a paging gesture. In the early days (namely with the DataGrid control), you had to handle these events in order to implement the selection or paging behavior. For the GridView and ListView controls, selection and paging are handled automatically by the control, so you don't normally have to handle the events, except if - as here - you want to run some custom logic.
Handling the Selection and Paged Events
During the selection event, I grabbed the ID of the data record that was being selected. This means you have to be working with records whose primary-key IDs you have. I figure that if you're selecting records for some purpose, it's quite likely that you're already doing that. I store the ID in a persistent page property (i.e., stored in the page's view state). The code for that property is in Listing 1. The handler for the selection method is in Listing 2.
Listing 1: Persistent property to store the currently selected ID (DataGrid)
  1. Public Property SelectedID() As Integer  
  2.     Get  
  3.         If ViewState("SelectedID"Is Nothing Then  
  4.             Return -1  
  5.         Else  
  6.             Return CInt(ViewState("SelectedID"))  
  7.         End If  
  8.     End Get  
  9.     Set(ByVal value As Integer)  
  10.         ViewState("SelectedID") = value.ToString()  
  11.     End Set  
  12. End Property  
Listing 2: Handler for Selected event (DataGrid)
  1. Protected Sub DataGrid1_SelectedIndexChanged(ByVal sender As Object, _  
  2.         ByVal e As System.EventArgs)  
  3.     Me.SelectedID = DataGrid1.DataKeys(DataGrid1.SelectedIndex)  
  4. End Sub  
In the paged event handler, I first reset the selected index to -1 to indicate that there is no selection. I get the items (rows) for the current page and walk through them, comparing the ID of each item to the ID that I stored earlier for the selected record. If they match, I select that record. If there's no match, nothing on the page is selected, which is the behavior I actually want. The code for the paged event handler is in Listing 3.
Listing 3: Handler for Paged event (DataGrid)
  1. Protected Sub DataGrid1_PageIndexChanged(ByVal source As Object, _  
  2.         ByVal e As DataGridPageChangedEventArgs)  
  3.     DataGrid1.CurrentPageIndex = e.NewPageIndex  
  4.     DataGrid1.SelectedIndex = -1  
  5.   
  6.     ' Need to rebind here to get current list of items. (Must rebind  
  7.     ' eventually anyway after resetting page index.)  
  8.     DataGrid1.DataBind()  
  9.   
  10.     ' See if selected item is on this page.  
  11.     For itemIndex As Integer = 0 To DataGrid1.Items.Count - 1  
  12.         If Me.SelectedID = DataGrid1.DataKeys(itemIndex) Then  
  13.             DataGrid1.SelectedIndex = itemIndex  
  14.             DataGrid1.DataBind() ' Need to bind (again) to reset selection.  
  15.             Exit For  
  16.         End If  
  17.     Next  
  18. End Sub  
Some Gotchas
There are a couple of gotchas. The issue that gave me the most problem was getting the list of items for the current page. I initially would just got the Items (Rows) collection, but as it turns out, at the beginning of the paging event, the Items collection is loaded with the previous page's records. The fix here was in the paged event handler to first call the grid control's DataBind method, which loads the Items collection with the correct list of records.
Another small gotcha for the DataGrid control was that if it happens that the selected record is on the current page, I have to call DataBind again in order to set the selected record. Calling DataBind more than once is suboptimal, but that's what seems to be required.
Adding De-selection
Another perhaps surprising "By Design" behavior is that clicking the Select button for a selected row doesn't unselect it. So while I was already customizing selection behavior, it seemed comparatively simple to add logic that would allow users to unselect a row by re-clicking the Select button.
In order to do that for the DataGrid control I stored the index of the selected item in another persistent property named SelectedIndex. This is also stored in view state; the code is virtually identical to the SelectedID property except for the name of the property and of the view state dictionary item. Then in the selection event handler, I compared the stored index and stored ID against the current index and current selection. If they matched, the user had clicked the Select button again, so I unselected the row. Listing 4 shows a version of the selection handler from Listing 2, but with the added code that enables de-selection.
Listing 4: Updated Selected handler with de-selection logic (DataGrid)
  1. Protected Sub DataGrid1_SelectedIndexChanged(ByVal sender As Object, _  
  2.         ByVal e As System.EventArgs)  
  3.     If e.SelectedID = DataGrid1.DataKeys(DataGrid1.SelectedIndex) Then  
  4.         Me.SelectedID = -1  
  5.         DataGrid1.SelectedIndex = -1  
  6.     Else  
  7.         Me.SelectedID = DataGrid1.DataKeys(DataGrid1.SelectedIndex)  
  8.     End If  
  9. End Sub  
Differences Between DataGrid, GridView, and ListView Controls
The code to perform all these tasks is similar for the DataGrid, GridView, and ListView controls. In fact, it's slightly easier in the GridView and ListView controls, because their richer object model exposes more information that's useful for this task.
A trivial difference is that in the DataGrid control, rows are in the Items collection, and the current page index is in the CurrentPageIndex property. In GridView and ListView, these are, respectively, the Rows collection and PageIndex property.
More interestingly, whereas the DataGrid control has only -ed events (SelectedIndexChanged, PageIndexChanged), newer controls support -ing events (SelectedIndexChanging, PageIndexChanging). For paging, I used the PageIndexChanged event in all cases, because I have to check for the selection after paging has finished. But in the GridView and ListView controls, the SelectedIndexChanging event gets the GridViewSelectEventArgs and ListViewSelectEventArgs objects (respectively) as parameters; these objects support a NewSelectedIndex property. This property makes it unnecessary to store the SelectedIndex property that I used for the DataGrid control, so I handle the selected event for DataGrid, and the selecting event for GridView and ListView.
Another small difference is that in the DataGrid control I had to explicitly manage paging by setting the current page to the target page index (which is available via a parameter passed to the paged method). As noted earlier, the GridView and ListView controls handle this behavior automatically, so you can leave out the couple of lines required to perform this task.
Finally, for the GridView and ListView controls, I didn't need to call DataBind explicitly after setting the selected index or page index. This isn't an optimization - DataBind is handled internally by the control, so it's not as if it's not invoked - but it does mean one less thing to code.
For completeness, I've included the code for all of the grid controls. The SelectedID property is the same in all cases. Listing 5 shows the selection handler for the GridView control. Note that this is the SelectedIndexChanging event, not the SelectedIndexChanged event.
Listing 5: Handler for Selecting event (GridView)
  1. Protected Sub GridView1_SelectedIndexChanging(ByVal sender As Object, _  
  2.         ByVal e As System.Web.UI.WebControls.GridViewSelectEventArgs)  
  3.     If Me.SelectedID = GridView1.DataKeys(e.NewSelectedIndex).Value Then  
  4.         Me.SelectedID = -1  
  5.         e.NewSelectedIndex = -1  
  6.     Else  
  7.         Me.SelectedID = GridView1.DataKeys(e.NewSelectedIndex).Value  
  8.     End If  
  9. End Sub  
Listing 6 shows the paged handler for the GridView control.
Listing 6: Handler for Paged event (GridView)
  1. Protected Sub GridView1_PageIndexChanged(ByVal sender As Object, _  
  2.         ByVal e As System.EventArgs)  
  3.     GridView1.SelectedIndex = -1  
  4.     GridView1.DataBind()  
  5.   
  6.     For itemIndex As Integer = 0 To GridView1.Rows.Count - 1  
  7.         If Me.SelectedID = GridView1.DataKeys(itemIndex).Value Then  
  8.             GridView1.SelectedIndex = itemIndex  
  9.             GridView1.DataBind() ' Need to bind (again) to reset selection.  
  10.             Exit For  
  11.         End If  
  12.     Next  
  13. End Sub  
Listing 7 shows the selecting handler for the ListView control.
Listing 7: Handler for Selecting event (ListView)
  1. Protected Sub ListView1_SelectedIndexChanging(ByVal sender As Object, _  
  2.         ByVal e As System.Web.UI.WebControls.ListViewSelectEventArgs)  
  3.     If e.SelectedID = ListView1.DataKeys(e.NewSelectedIndex).Value Then  
  4.         Me.SelectedID = -1  
  5.         e.NewSelectedIndex = -1  
  6.     Else  
  7.         Me.SelectedID = ListView1.DataKeys(e.NewSelectedIndex).Value  
  8.     End If  
  9. End Sub  
Listing 8 shows the paged handler for the ListView control.
Listing 8: Handler for Paged event (ListView)
  1. Protected Sub ListView1_PagePropertiesChanged(ByVal sender As Object, _  
  2.         ByVal e As System.EventArgs)  
  3.     ListView1.SelectedIndex = -1  
  4.     Me.SelectedIndex = -1  
  5.     ListView1.DataBind()  
  6.   
  7.     For i As Integer = 0 To ListView1.Items.Count - 1  
  8.         If Me.SelectedID = ListView1.DataKeys(i).Value Then  
  9.             ListView1.SelectedIndex = i  
  10.             Me.SelectedIndex = i  
  11.             Exit For  
  12.         End If  
  13.     Next  
  14. End Sub  
Summary
By default, the selection behavior for grid controls that support paging does not work the way you probably want it to. However, by adding a small amount of code in selection and paging event handlers, you tweak the controls to display (and to not display) selection when they should.
About Mike Pope
Mike Pope has been a member of the ASP.NET User Education team since version 1.0 of the product.

 

Source: http://dotnetslackers.com/articles/gridview/persistingselectioninaspnetgridcontrolswhilepaging.aspx

No comments: