Reputation: 242
I have successfully retained selection of a single row when sorting columns in a DataGridView
, but this time I intend to track the selection of multiple rows when sorting columns in the DataGridView
. I've seen one post on this topic referring to DataGrid
, but the answer was unhelpful.
First I tried quite simply just making a copy of the previous selected rows collection, then making a copy of the current selected rows collection. This did not work however, because when you sort a column, I noticed the SelectionChanged
event fires twice before the Sorted
event fires once.
Therefore I devised a class which stores three sequential copies, and after sorting, it should just re-select the earliest of the 3 copies. The UpdateSelection
sub is called on SelectionChanged
event, and SelectPrevious
sub is called on Sorted
event.
The problem is this: The below code seems to work while selecting items. The Debug.Print
results step back correctly each time an item is selected. BUT as soon as I Sort, all of these array copies are cleared on the first SelectionChanged
event. I really don't understand how.
Unless I'm mistaken, as each array is a copy, it should remain unaffected, correct? Even though it clears m_CurrentRows
, it shouldn't clear the m_PreviousRows0, 1, 2
. It should step back one at a time, same way it does when rows are selected.
I'm either looking for a way to not have the all previous selection arrays completely deleted - this is baffling in itself.
Or a way to store the selection after calling Sort
, but before Sorted
fires. This isn't obvious, and there's no way to anticipate when a user might click on a column header. It seems trying to track the selection every time anything is selected or deselected is not going to work, so if there's a way to intercept it (as hinted at below) then that would be even better, but I would need to know how.
NB - the module with extensions - if I've missed any let me know and I'll include. Also, while checking I'm using cell value 2, so make sure data set has at least 3 columns.
Class clsDataGridViewSelectedRowTracker
Private ReadOnly m_DataGridView As DataGridView
Private ReadOnly m_CurrentRows As List(Of DataGridViewRow)
Private m_PreviousRows0() As DataGridViewRow
Private m_PreviousRows1() As DataGridViewRow
Private m_PreviousRows2() As DataGridViewRow
''' <summary>
''' Create new instance of DataGridView Selected Row Tracker
''' </summary>
''' <param name="dataGridView">Instance of DataGridView - SelectionMode must be FullRowSelect</param>
Friend Sub New(ByRef dataGridView As DataGridView)
m_DataGridView = dataGridView
m_CurrentRows = New List(Of DataGridViewRow)
m_PreviousRows0 = {}
m_PreviousRows1 = {}
m_PreviousRows2 = {}
If Not m_DataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect Then
m_DataGridView.SelectionMode=DataGridViewSelectionMode.FullRowSelect
End If
End Sub
''' <summary>
''' Updates selection tracker with current and previous selection values
''' </summary>
Friend Sub UpdateSelection()
'Debugging the current issue - displays all values each time an item is selected
If m_CurrentRows.Count > 0 AndAlso m_PreviousRows2.Length > 0 Then
Debug.Print("{0} - {1} - {2} - {3}", "C: " & m_CurrentRows(0).Value.Cell(2), "0: " & m_PreviousRows0(0).Value.Cell(2), "1: " & m_PreviousRows1(0).Value.Cell(2), "2: " & m_PreviousRows2(0).Value.Cell(2))
ElseIf m_CurrentRows.Count > 0 AndAlso m_PreviousRows1.Count > 0 Then
Debug.Print("{0} - {1} - {2} - {3}", "C: " & m_CurrentRows(0).Value.Cell(2), "0: " & m_PreviousRows0(0).Value.Cell(2), "1: " & m_PreviousRows1(0).Value.Cell(2), "2: ")
ElseIf m_CurrentRows.Count > 0 AndAlso m_PreviousRows0.Count > 0 Then
Debug.Print("{0} - {1} - {2} - {3}", "C: " & m_CurrentRows(0).Value.Cell(2), "0: " & m_PreviousRows0(0).Value.Cell(2), "1: ", "2: ")
ElseIf m_CurrentRows.Count > 0 Then
Debug.Print("{0} - {1} - {2} - {3}", "C: " & m_CurrentRows(0).Value.Cell(2), "0: ", "1: ", "2: ")
End If
'Back up current rows and previous 2 instances
If m_PreviousRows1 IsNot Nothing AndAlso m_PreviousRows1.Length > 0 Then
ReDim m_PreviousRows2(m_PreviousRows1.Length - 1)
Call m_PreviousRows1.CopyTo(m_PreviousRows2, 0)
End If
If m_PreviousRows0 IsNot Nothing AndAlso m_PreviousRows0.Length > 0 Then
ReDim m_PreviousRows1(m_PreviousRows0.Length - 1)
Call m_PreviousRows0.CopyTo(m_PreviousRows1, 0)
End If
If m_CurrentRows.Count > 0 Then
ReDim m_PreviousRows0(m_CurrentRows.Count - 1)
Call m_CurrentRows.CopyTo(m_PreviousRows0, 0)
End If
'Get currently selected rows, if any
Dim m_selectedRows As DataGridViewSelectedRowCollection = m_DataGridView.SelectedRows
'Clear list of current rows
Call m_CurrentRows.Clear()
'Add each selected item to list of currently selected rows
For Each EachSelectedRow As DataGridViewRow In m_selectedRows
Call m_CurrentRows.Add(EachSelectedRow)
Next
End Sub
''' <summary>
''' Attempts to select the previously selected rows
''' </summary>
Friend Sub SelectPrevious()
'Ensure Grid exists and contains rows
If m_DataGridView IsNot Nothing AndAlso m_DataGridView.RowCount > 0 Then
'Visible
Dim m_VisibleRow As DataGridViewRow = Nothing
'Compare each row value against previous row values
For Each EachDataGridViewRow As DataGridViewRow In m_DataGridView.Rows
'Use the level two instance of previous rows after sorting
For Each EachPreviousRow As DataGridViewRow In m_PreviousRows2
If EachPreviousRow.Value.Row.Equivalent(EachDataGridViewRow.Value.Row) Then
'Select the row
EachDataGridViewRow.Selected = True
'Only store visible row for the first selected row
If m_VisibleRow Is Nothing Then m_VisibleRow = EachDataGridViewRow
End If
Next 'Each Previous Selected Row
Next 'Each Row
'Ensure first selected row is always visible
If m_VisibleRow IsNot Nothing AndAlso Not m_VisibleRow.Displayed Then
If (m_VisibleRow.Index - m_DataGridView.DisplayedRowCount(True) \ 2) > 0 Then
'Place row in centre of DataGridView
m_DataGridView.FirstDisplayedScrollingRowIndex = m_VisibleRow.Index - m_DataGridView.DisplayedRowCount(True) \ 2
Else
'Place row at top of DataGridView
m_DataGridView.FirstDisplayedScrollingRowIndex = m_VisibleRow.Index
End If
End If
End If
End Sub
End Class
Module Extensions
''' <summary>
''' Determines whether the specified string is equivalent to current string (Not case sensitive)
''' </summary>
''' <param name="str1">The string to compare with the following string</param>
''' <param name="str2">The second string to compare</param>
''' <returns></returns>
<DebuggerStepThrough()>
<Extension()>
Friend Function Equivalent(ByVal str1 As String, str2 As String) As Boolean
Return str1.ToUpper.Equals(str2.ToUpper)
End Function
''' <summary>
''' Quick extension to speed up proceedings
''' </summary>
''' <param name="dgvr"></param>
''' <param name="cellindex"></param>
''' <returns></returns>
<Extension>
Friend Function CellValueString(ByRef dgvr As DataGridViewRow, ByVal cellindex As Integer) As String
If dgvr Is Nothing Then Return String.Empty
If dgvr.Cells Is Nothing Then Return String.Empty
If cellindex >= dgvr.Cells.Count Then Return String.Empty
If dgvr.Cells(cellindex).Value Is Nothing Then Return String.Empty
Return dgvr.Cells(cellindex).Value.ToString
End Function
End Module
Upvotes: 1
Views: 627
Reputation: 54417
This code worked for me and should work regardless of the data source:
Private Sub SortGrid(direction As ListSortDirection)
Dim selectedItems = DataGridView1.SelectedRows.
Cast(Of DataGridViewRow)().
Select(Function(dgvr) dgvr.DataBoundItem).
ToArray()
DataGridView1.Sort(DataGridView1.Columns(0), direction)
For Each row As DataGridViewRow In DataGridView1.Rows
row.Selected = selectedItems.Contains(row.DataBoundItem)
Next
End Sub
It's worth noting that the Sort
methods of the DataGridView
class are Overridable
, so you could create your own custom class that inherits DataGridView
and adds that functionality:
Imports System.ComponentModel
Public Class DataGridViewEx
Inherits DataGridView
Public Overrides Sub Sort(comparer As IComparer)
Dim selectedItems = GetSelectedItems()
MyBase.Sort(comparer)
ReselectRows(selectedItems)
End Sub
Public Overrides Sub Sort(dataGridViewColumn As DataGridViewColumn, direction As ListSortDirection)
Dim selectedItems = GetSelectedItems()
MyBase.Sort(dataGridViewColumn, direction)
ReselectRows(selectedItems)
End Sub
Private Function GetSelectedItems() As Object()
Return If(DataSource Is Nothing,
Nothing,
SelectedRows.Cast(Of DataGridViewRow)().
Select(Function(dgvr) dgvr.DataBoundItem).
ToArray())
End Function
Private Sub ReselectRows(selectedItems As Object())
If selectedItems IsNot Nothing Then
For Each row As DataGridViewRow In Rows
row.Selected = selectedItems.Contains(row.DataBoundItem)
Next
End If
End Sub
End Class
Use that control instead of a regular DataGridView
and it will just work.
Upvotes: 3
Reputation: 74605
Or, you could just have a boolean in the underlying datatable and a checkbox column - let the user tick boxes in the column, or maybe select rows and click a button to "tick selected rows", and then give them more buttons to "perform delete of ticked rows" etc
I generally prefer this approach if I have some sort of mixed work mode multi select going on, because multiple selections are a fickle/easily lost thing and users generally don't understand how to combine shift/ctrl click to easily select multiple contiguous ranges.. Easier just to give them a system where they can choose some multiple rows and a button to mark those rows as interesting for further actions, then further actions only carry out on marked rows.
If you think your users might not understand, and just highlight some rows and click an action button, perhaps you can pre-tick all the highlighted rows if there are no ticked rows when an action button is clicked
Ultimately, the way we think a user will understand our program and use its interface is very different to how they do. I spent weeks creating a beautiful and helpful UI for a program once, including a mass-upload facility from an excel file, and was reasonably horrified to see that they completely ignored the UI and even for loading a single user into the system, would hit up excel, type up his details into a single row and save it as a spreadsheet, then import one user; this bypassed all of the autocomplete, lookups and other suggestions the UI made but it taught an important lesson to never underestimate the difference between how you envisage a program will be used vs how it will actually be used
Upvotes: 0