问题
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