DataGridView排序时是否可以保留多行选择?

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

我在DataGridView中对列进行排序时成功保留了单行的选择,但是这次我打算在对列进行排序时跟踪多行的选择在 DataGridView。我看到一个 post 关于这个话题,提到 DataGrid,但答案没有帮助。

首先,我尝试非常简单地复制先前选定的行 collection,然后复制当前选定的行 collection。但是这不起作用,因为当您对列进行排序时,我注意到 SelectionChanged 事件在 Sorted 事件触发一次之前触发 两次

因此我设计了一个class存储三个顺序副本,并且在排序之后,它应该re-select 3 个副本中最早的一个。 UpdateSelection sub 在 SelectionChanged 事件上调用,SelectPrevious sub 在 Sorted 事件上调用。


然而

问题是:下面的代码似乎在选择项目时有效。每次选择一个项目时,Debug.Print 结果都会正确后退。 BUT 一旦我排序,所有 这些数组副本在第一个 SelectionChanged 事件中被清除。我真的不明白。

除非我弄错了,因为每个数组都是一个副本,它应该不受影响,对吗?即使它清除 m_CurrentRows,也不应清除 m_PreviousRows0, 1, 2。它应该一次后退一个,与选择行时的方式相同。

我在找什么

我正在寻找一种不完全删除所有先前选择数组的方法 - 这本身就令人费解。

或者在调用 Sort 之后但在 Sorted 触发之前存储选择的方法。这并不明显,并且无法预测用户何时会点击列 header。似乎每次选择或取消选择时都试图跟踪选择 是行不通的,所以如果有办法拦截它(如下所示),那就更好了,但我需要知道怎么做。


注意 - 带有扩展的模块 - 如果我遗漏了任何内容,请告诉我,我会包括在内。此外,在检查我使用的单元格值 2 时,请确保数据集至少有 3 列。

    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

或者,您可以在基础数据表中只包含一个布尔值和一个复选框列 - 让用户在该列或 select 行中勾选复选框,然后单击按钮 "tick selected rows",然后给他们更多按钮 "perform delete of ticked rows" etc

如果我有某种混合工作模式 multi select,我通常更喜欢这种方法,因为多个 selection 是一个 fickle/easily 丢失的东西,用户通常不会不明白如何组合 shift/ctrl 单击以轻松地 select 多个连续范围.. 更容易只是给他们一个系统,他们可以在其中选择一些多行和一个按钮来将这些行标记为对进一步操作感兴趣,然后进一步的行动只对标记的行进行。

如果您认为您的用户可能不理解,只是突出显示某些行并单击操作按钮,也许您可​​以预先勾选所有突出显示的行,如果在单击操作按钮时没有勾选的行

最终,我们认为用户理解我们的程序和使用其界面的方式与他们的做法大不相同。我花了数周时间为一个程序创建了一个漂亮且有用的 UI,包括 excel 文件的批量上传功能,看到他们完全忽略 UI 和即使是将单个用户加载到系统中,也会点击 excel,将他的详细信息输入一行并将其保存为电子表格,然后导入一个用户;这绕过了 UI 提出的所有自动完成、查找和其他建议,但它教会了一个重要的教训,即永远不要低估程序的使用方式与实际使用方式之间的差异

此代码对我有效,并且无论数据源如何都应该有效:

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

值得注意的是 DataGridView class 的 Sort 方法是 Overridable,因此您可以创建自己的自定义 class 继承 DataGridView 并添加该功能:

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

使用该控件而不是常规控件 DataGridView,它将正常工作。