Custom comparer datagridview sort

一个人想着一个人 提交于 2021-02-04 16:35:27

问题


I have a datagridview with a bindingsource as datasource, and the bindingsource has a datatable as a datasource. Some columns are strings but I want them to be sorted in a specific way.

The grid sorts them as 1, 10, 10,0 44a, 6c.

But I want them to sorted: 1, 6c, 10, 44a, 100 as if i would take the numbers only from the values and sort them accordingly.

Is there a way I cand add a custom comparer when certain columns are being sorted? Any other soulutions would be ok if the grid, bindingsource, datatable schema is not changed.


回答1:


Is there a way I can add a custom comparer Yes!

When the DGV is bound to a DataSource, you have to act on (sort) the source not the DGV itself. This rules out some options like using the SortCompare event. The method below uses a DataView.

First, I started with the Natural String Sorter from this answer and made a few changes:

Imports System.Runtime.InteropServices

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

That Comparer can be used in a variety ways as evidenced from the linked question. It is typically used for things like a List of file names. Since the sort target here is DB data, a couple of lines were added to Compare for when it encounters null data. (The OP, mvaculisteanu, discovered it was slow when null values were passed).

This would also work, handled as a separate step other edge cases can be easily added:

Return (mySortFlipper * NativeMethods.NaturalStringCompare(If(x, ""), If(y,""))

I don't know how you are using the BindingSource, so I had to make some guesses on the configuration. My test DataTable has 3 columns, #1 is set to programmatic to implement the comparer. Form level object variables used (so you understand my configuration - hopefully it is similar):

Private dgvDV As DataView
Private dgvBS As BindingSource

' config:
dgvDV = New DataView(dgvDT)

dgvBS = New BindingSource()
dgvBS.DataMember = "myDT"
dgvBS.DataSource = dgvDT

dgv2.Columns(0).SortMode = DataGridViewColumnSortMode.Automatic
dgv2.Columns(1).SortMode = DataGridViewColumnSortMode.Programmatic
dgv2.Columns(2).SortMode = DataGridViewColumnSortMode.Automatic

The magic, such as it is, happens in the ColumnHeaderMouseClick event:

Private SortO As SortOrder = SortOrder.Ascending
Private Sub dgv2_ColumnHeaderMouseClick(sender As Object...etc

    ' the special column we want to sort:
    If e.ColumnIndex = 1 Then
        ' create new DV
        dgvDV = DGVNaturalColumnSort("Text", SortO)

        ' reset the BindingSource:
        dgvBS.DataSource = dgvDV
        ' update glyph
        dgv2.Columns(1).HeaderCell.SortGlyphDirection = SortO

        ' flip order for next time:
        SortO = If(SortO = SortOrder.Ascending, SortOrder.Descending, SortOrder.Ascending)
    End If
End Sub

Then, a helper function which implements the sort and create a new DataView:

Private Function DGVNaturalColumnSort(colName As String, sortt As SortOrder) As DataView
    Dim NComparer As New NaturalStringComparer(sortt)
    Dim tempDT = dgvDV.Table.AsEnumerable().
        OrderBy(Function(s) s.Field(Of String)(colName), NComparer).
        CopyToDataTable

    Return New DataView(tempDT)
End Function

Because you pass the name of the column, it should be easy to use when there are multiple such columns. Results:

Sort None on top, then Sort Asc and Desc below

User changes to the column(s) such as the order and width are preserved. This also works just fine without a BindingSource. Just use your DataView as the DataSource:

  dgvBS.DataSource = dgvDV

Using a DataTable as the DataSource could be problematic and "heavier" since you would have to copy the table. A DataView makes this quite simple.


I also found this AlphaNumeric sorter for java. Being curious, I converted it to .NET to compare them. It works well but not quite the same. Given the same starting point, 25-35 of 1000 sequences will typically come out differently:

 PInvoke:  03, 03, 03s, 3A
Alphanum:  03, 3A...3RB, 03s, 3X 

Its not totally wrong, the 03s is in the right area and the results synch back up for awhile after that. It also treats leading dashes differently and is a bit slower than PInvoke. It does handle Nothing values fine though.




回答2:


Yes, but you would I think you would have to have another column because the computer reads the first number then the second. Which is why when you have 10 it reads this before 6. (1 is lower than 6) One way around this which I have done in the past is to have leading Zeros. So in 1 hidden column you would have leading zeros Example: 000006, 000100, then the visible column for the user you would have your original data but the sort column would be your hidden one.




回答3:


This looked rather simple at first, then really hard and tedious and finally turned out to be really simple after all.. Hooray for LINQ!

First write a function that converts your string column to numeric values:

int noLetters(string text)
{
    char c = text[text.Length - 1];
    if (c < '0' || c > '9') text = text.Substring(0, text.Length - 1);
    int n = 0;
    Int32.TryParse(text, out n);
    return n;
}

Note: The function only tests the last letter and chops off anything that isn't a digit. Please do check this for your rules and add your exception handling!

Now order the data with the new function into a DataView via an EnumerableRowCollection:

EnumerableRowCollection<DataRow> sortedQuery =
    from row in dt.AsEnumerable()
    orderby noLetters(row.Field<string>("yourColumnName"))
    select row ;

DataView sortedView = sortedQuery.AsDataView();
yourBindingSource.DataSource = sortedView ;

To trigger it via a click on the column header simply code the ColumnHeaderMouseClick event..

Of course you can add logic to switch between orderby and orderbydescending



来源:https://stackoverflow.com/questions/35361056/custom-comparer-datagridview-sort

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