こんにちは!
今回は「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 = employeesDataGridView 側の設定も忘れずに!
DataGridView の各列が「クリックしてソートできる」ようにするには、SortMode を Automatic に設定してあげましょう。
DataGridView1.Columns("EmployeeID").SortMode = DataGridViewColumnSortMode.Automatic
DataGridView1.Columns("Name").SortMode = DataGridViewColumnSortMode.Automaticたったこれだけです!
補足:ボタン列の扱いについて
ちなみに、ボタン列(DataGridViewButtonColumn)は値を持たないので、基本的にはソートの対象外になります。
「ボタンを押した回数」とか「フラグ値」など、ソートしたい情報があれば、それを別の隠し列に入れてそちらでソートする、というのが現実的な回避策です。
まとめ
BindingList<T>のままだと DataGridView のソートには非対応SortableBindingList<T>を自作すれば、ソート・フィルター可能なリストに早変わり- 列の
SortModeを設定すれば、あとはクリックで並び替え!
ソートができるようになるだけで、一覧表示の使い勝手がぐっと上がりますよね。
というわけで、カスタムな BindingList で DataGridView をもっと便利にしてみてください〜!
