torpid prey
torpid prey

Reputation: 242

Is it possible to retain the selection of multiple rows when sorting DataGridView?

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.


However

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.

What I'm looking for

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

Answers (2)

jmcilhinney
jmcilhinney

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

Caius Jard
Caius Jard

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

Related Questions