How can I natural string sort a datagridview that is databound to a datatable

老子叫甜甜 提交于 2021-02-11 06:10:05

问题


In my program, I have a datagridview that is bound to a datatable using a binding source. What I would like to accomplish to be able to sort the datagridview by a column using a natural string sort.

Sample Column Data:

XAB-1
XAB-2
XAB-11
XAB-3
XAB-1A
XAB-10
XAB-1B

Desired Result:

XAB-1
XAB-1A
XAB-1B
XAB-2
XAB-3
XAB-10
XAB-11

I have tried using the datagridview.sort method passing in a natural string Icomparer, but the sort function cannot be used when the datagridview is databound.

I have also tried the solution from this thread Link, but involves changing the bindingsource source from the datatable to a dataview. This is an issue as data is being updated in the datatable and is not being reflected in the dataview.

Is there some way that I can perform a natural string sort while still maintain the binding source to the datatable? Or is there some way I could use the solution in the link above with the dataview, but have the dataview somehow sync the data with the datatable?

Here is the Icomparer I was trying to use:

Imports System.Runtime.InteropServices
Imports System.Text.RegularExpressions

Partial Class NativeMethods
    <DllImport("shlwapi.dll", CharSet:=CharSet.Unicode)>
    Private Shared Function StrCmpLogicalW(s1 As String, s2 As String) As Int32
    End Function

    Friend Shared Function NaturalStringCompare(str1 As String, str2 As String) As Int32
        Return StrCmpLogicalW(str1, str2)
    End Function
End Class

Public Class NaturalStringComparer
    Implements IComparer(Of String)
    Private mySortFlipper As Int32 = 1

    Public Sub New()

    End Sub

    Public Sub New(sort As SortOrder)
        mySortFlipper = If(sort = SortOrder.Ascending, 1, -1)
    End Sub

    Public Function Compare(x As String, y As String) As Integer _
             Implements IComparer(Of String).Compare

        ' convert DBNull to empty string
        Dim x1 = If(String.IsNullOrEmpty(x), String.Empty, x)
        Dim y1 = If(String.IsNullOrEmpty(y), String.Empty, y)

        Return (mySortFlipper * NativeMethods.NaturalStringCompare(x1, y1))
    End Function
End Class

Public Class NumStrCmp
    Implements IComparer

    Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements IComparer.Compare
        Dim regex As Regex = New Regex("(?<NumPart>\d+)(?<StrPart>\D*)", RegexOptions.Compiled)
        Dim mx = regex.Match(x.ToString)
        Dim my = regex.Match(y.ToString)
        Dim ret = Integer.Parse(mx.Groups("NumPart").Value).CompareTo(Integer.Parse(my.Groups("NumPart").Value))
        If ret <> 0 Then Return ret
        Return mx.Groups("StrPart").Value.CompareTo(my.Groups("StrPart").Value)
    End Function
End Class

回答1:


API declaration:

Imports System.Runtime.InteropServices

Public Module NativeMethods

    <DllImport("shlwapi.dll", CharSet:=CharSet.Unicode)>
    Public Function StrCmpLogicalW(x As String, y As String) As Integer
    End Function

End Module

Custom comparer:

Public Class NaturalStringComparer
    Implements IComparer(Of String)

    Public Function Compare(x As String, y As String) As Integer Implements IComparer(Of String).Compare
        Return NativeMethods.StrCmpLogicalW(x, y)
    End Function

End Class

The following test code requires a form with a DataGridView and a BindingSource with default names:

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        'Create standard table.
        Dim table As New DataTable

        With table.Columns
            .Add("Id", GetType(Integer))
            .Add("Code", GetType(String))
        End With

        With table.Rows
            .Add(1, "XAB-1")
            .Add(2, "XAB-2")
            .Add(3, "XAB-11")
            .Add(4, "XAB-3")
            .Add(5, "XAB-1A")
            .Add(6, "XAB-10")
            .Add(7, "XAB-1B")
        End With

        'Add order column.
        table.Columns.Add("Order", GetType(Integer))

        'Set the row order.
        OrderTableRows(table, "Code", "Order")

        'Bind and display in appropriate sort order.
        BindingSource1.DataSource = table
        BindingSource1.Sort = "Order"
        DataGridView1.DataSource = BindingSource1
    End Sub

    Private Sub OrderTableRows(table As DataTable, sortColumnName As String, orderColumnName As String)
        Dim rows = table.Rows.Cast(Of DataRow)().ToArray()

        'Get the value to sort by for each row.
        Dim sortValues = Array.ConvertAll(rows, Function(row) row.Field(Of String)(sortColumnName))

        'Sort the rows by the sort values using a natural comparison.
        Array.Sort(sortValues, rows, New NaturalStringComparer)

        'Number the rows sequentially based on the sort order.
        For i = 0 To rows.GetUpperBound(0)
            rows(i)(orderColumnName) = i
        Next
    End Sub

End Class

That will display the records in the order you want. If you ever make any changes to the Code column, i.e. edit an existing row or add a new row, then you would need to call OrderTableRows again and the data would resort correctly.

In a real app, you may want to not display that Order column, which you can do by explicitly hiding it or else add your grid columns in the designer and omit that one, then set AutoGenerateColumns to False in code. If you want to be able to click a grid column header to sort then you will need to set the SortMode to Programmatic and then use this sorting method behind the scenes.

EDIT:

I have extended the example above to enable sorting when clicking the Code column header cell. Firstly, I added Id and Code columns in the designer. Here's the code that that generated:

'
'idColumn
'
Me.idColumn.DataPropertyName = "Id"
Me.idColumn.HeaderText = "Id"
Me.idColumn.Name = "idColumn"
'
'codeColumn
'
Me.codeColumn.DataPropertyName = "Code"
Me.codeColumn.HeaderText = "Code"
Me.codeColumn.Name = "codeColumn"
Me.codeColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic

You can add the columns in the designer and then set those properties accordingly. I then disabled automatic generation of columns, so that no grid column would be generated for the Order table column:

'Bind and display in appropriate sort order.
BindingSource1.DataSource = table
BindingSource1.Sort = "Order"
DataGridView1.AutoGenerateColumns = False
DataGridView1.DataSource = BindingSource1

Finally, I detected clicks on the Code column header and sorted the BindingSource by the Order column. If sorting was currently by a different column, I sorted in ascending order, otherwise I switched the direction:

Private Sub DataGridView1_ColumnHeaderMouseClick(sender As Object, e As DataGridViewCellMouseEventArgs) Handles DataGridView1.ColumnHeaderMouseClick
    Dim column = DataGridView1.Columns(NameOf(codeColumn))

    If e.ColumnIndex = column.Index Then
        'Sort by Order as a proxy for Code. Use ascending order by default.
        Dim sort = "Order"
        Dim direction = SortOrder.Ascending

        If DataGridView1.SortedColumn Is Nothing AndAlso
           BindingSource1.Sort?.StartsWith("Order", StringComparison.InvariantCultureIgnoreCase) AndAlso
           Not BindingSource1.Sort?.EndsWith("DESC", StringComparison.InvariantCultureIgnoreCase) Then
            'Already sorted in ascending direction by Order as a proxy for Code so reverse direction.
            sort &= " DESC"
            direction = SortOrder.Descending
        End If

        BindingSource1.Sort = sort
        column.HeaderCell.SortGlyphDirection = direction
    End If
End Sub

To the user, it looks just like there is no Order column and that the Code column is automatically sorted naturally.



来源:https://stackoverflow.com/questions/61646366/how-can-i-natural-string-sort-a-datagridview-that-is-databound-to-a-datatable

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!