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