[VB.NET]DataGridViewでボタン列やテキスト列をソートする方法

こんにちは!
今回は「Windows Forms の DataGridView で、ボタン列やテキスト列をヘッダークリックでソートするにはどうすればいいの?」というテーマでお届けします。

DataGridViewって便利なんですが、ちょっとひねったことをしようとすると「おや?」となること、ありますよね。特に、BindingList<T> をデータソースにしていると、ソートができない……なんて壁にぶち当たる方も多いのではないでしょうか。

というわけで、今回は「ソート可能な BindingList」を作って、DataGridViewでのソートをばっちり実現する方法をご紹介します!


BindingList ではソートできない?

まず前提ですが、BindingList<T> は便利なバインディング用リストですが、実はソートやフィルタリングには対応していません。

つまり、DataGridViewのヘッダーをカチカチしても並び替わらない!

……という仕様です。

でもご安心を。BindingList<T> をちょっと拡張してあげれば、ソートもフィルタも可能になりますよ。


SortableBindingList を作ってみよう

以下のように BindingList<T> を継承して、ソート機能を持った SortableBindingList<T> を自作します。

Imports System.ComponentModel
Imports System.Reflection

Public Class SortableBindingList(Of T)
    Inherits BindingList(Of T)
    Implements IBindingListView

    Private isSortedValue As Boolean
    Private sortDirectionValue As ListSortDirection
    Private sortPropertyValue As PropertyDescriptor
    Private originalList As List(Of T)
    Private filterValue As String

    Public Sub New()
        MyBase.New()
        originalList = New List(Of T)()
    End Sub

    Public Sub New(list As IList(Of T))
        MyBase.New(list)
        originalList = New List(Of T)(list)
    End Sub

    Protected Overrides ReadOnly Property SupportsSortingCore() As Boolean
        Get
            Return True
        End Get
    End Property

    Protected Overrides ReadOnly Property IsSortedCore() As Boolean
        Get
            Return isSortedValue
        End Get
    End Property

    Protected Overrides ReadOnly Property SortPropertyCore() As PropertyDescriptor
        Get
            Return sortPropertyValue
        End Get
    End Property

    Protected Overrides ReadOnly Property SortDirectionCore() As ListSortDirection
        Get
            Return sortDirectionValue
        End Get
    End Property

    Protected Overrides Sub ApplySortCore(prop As PropertyDescriptor, direction As ListSortDirection)
        sortPropertyValue = prop
        sortDirectionValue = direction

        Dim list As List(Of T) = Me.Items

        list.Sort(Function(x, y)
                      Dim value1 As Object = prop.GetValue(x)
                      Dim value2 As Object = prop.GetValue(y)
                      Dim result As Integer = Comparer.Default.Compare(value1, value2)
                      If direction = ListSortDirection.Descending Then
                          result = -result
                      End If
                      Return result
                  End Function)

        isSortedValue = True
        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
    End Sub

    Protected Overrides Sub RemoveSortCore()
        Me.Items.Clear()
        For Each item In originalList
            Me.Items.Add(item)
        Next
        isSortedValue = False
    End Sub

    ' 簡易フィルター対応
    Public ReadOnly Property SupportsFiltering As Boolean Implements IBindingListView.SupportsFiltering
        Get
            Return True
        End Get
    End Property

    Public Property Filter As String Implements IBindingListView.Filter
        Get
            Return filterValue
        End Get
        Set(value As String)
            filterValue = value
            UpdateFilter()
        End Set
    End Property

    Private Sub UpdateFilter()
        Dim items = originalList.AsEnumerable()

        If Not String.IsNullOrEmpty(filterValue) Then
            Dim parts = filterValue.Split("="c)
            If parts.Length = 2 Then
                Dim propName = parts(0).Trim()
                Dim propValue = parts(1).Trim().Trim("'"c)

                Dim prop = TypeDescriptor.GetProperties(GetType(T))(propName)
                If prop IsNot Nothing Then
                    items = items.Where(Function(x) prop.GetValue(x).ToString() = propValue)
                End If
            End If
        End If

        Me.Items.Clear()
        For Each item In items
            Me.Items.Add(item)
        Next

        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
    End Sub

    ' 未使用機能の実装(今回は使いません)
    Public ReadOnly Property SupportsAdvancedSorting As Boolean Implements IBindingListView.SupportsAdvancedSorting
        Get
            Return False
        End Get
    End Property

    Public Sub ApplySort(sorts As ListSortDescriptionCollection) Implements IBindingListView.ApplySort
        Throw New NotSupportedException()
    End Sub

    Public Sub RemoveFilter() Implements IBindingListView.RemoveFilter
        Me.Filter = Nothing
    End Sub

    Public ReadOnly Property SortDescriptions As ListSortDescriptionCollection Implements IBindingListView.SortDescriptions
        Get
            Return Nothing
        End Get
    End Property
End Class

このクラスのポイント

  • ApplySortCore で指定プロパティの昇順・降順ソートができます。
  • フィルターにも対応していますが、簡易的な実装です。
  • ソート解除するときには originalList から元の並びに戻せます。

実際に使ってみる

たとえばこんな Employee クラスを用意して……

Public Class Employee
    Public Property EmployeeID As Integer
    Public Property Name As String
End Class

データを作ってバインド!

Dim employees As New SortableBindingList(Of Employee)()
employees.Add(New Employee() With {.EmployeeID = 1, .Name = "Alice"})
employees.Add(New Employee() With {.EmployeeID = 2, .Name = "Bob"})
employees.Add(New Employee() With {.EmployeeID = 3, .Name = "Charlie"})

DataGridView1.DataSource = employees

DataGridView 側の設定も忘れずに!

DataGridView の各列が「クリックしてソートできる」ようにするには、SortModeAutomatic に設定してあげましょう。

DataGridView1.Columns("EmployeeID").SortMode = DataGridViewColumnSortMode.Automatic
DataGridView1.Columns("Name").SortMode = DataGridViewColumnSortMode.Automatic

たったこれだけです!


補足:ボタン列の扱いについて

ちなみに、ボタン列(DataGridViewButtonColumn)は値を持たないので、基本的にはソートの対象外になります。

「ボタンを押した回数」とか「フラグ値」など、ソートしたい情報があれば、それを別の隠し列に入れてそちらでソートする、というのが現実的な回避策です。


まとめ

  • BindingList<T> のままだと DataGridView のソートには非対応
  • SortableBindingList<T> を自作すれば、ソート・フィルター可能なリストに早変わり
  • 列の SortMode を設定すれば、あとはクリックで並び替え!

ソートができるようになるだけで、一覧表示の使い勝手がぐっと上がりますよね。

というわけで、カスタムな BindingList で DataGridView をもっと便利にしてみてください〜!