DataGridViewにEntity/DTO入りのListを繋げてSort/Filterしたいので、ライブラリ探し&サンプル作成

S2Dao.NETを使ってて悩んだのはここ。
せっかくモデルクラスに閉じ込めて固くしたデータを、表示/編集のためだけにDataTableに展開するのは勿体ない。

.NETフレームワークには、ソート/フィルターするためのIBindingListViewインターフェースはあるが、これを実装したクラスはDataViewだけ。そしてDataViewはDataTable専用。標準フレームワークにDataObjectViewとか用意しといて欲しいぞ。

定石ライブラリはあるかと探したが見つからず。フル装備っぽいBindingListViewというクラスを提供するライブラリもある。が、ちょっと大きいか。
http://blw.sourceforge.net/


以下は最低限の機能を実装したサンプルのよう。
http://lemoncake.wordpress.com/2007/06/13/filtering-on-a-list-bindingsource/
これをVB.NET用に移植してみた。このままでは実用にはならない(BindingListから継承してるのでItems周りの制御を奪うのが大変。IBindingListを自分で実装すべきなんだろう)がサンプルとして。

Imports System
Imports System.Collections.Generic
Imports System.Collections
Imports System.Text
Imports System.ComponentModel

Namespace Sample

    '======================================================================
    'データオブジェクトビューのサンプル(DataViewの一般Object版的ななにか)
    '
    'via. http://lemoncake.wordpress.com/2007/06/13/filtering-on-a-list-bindingsource/
    '
    Public Class DataObjectView(Of T)
        Inherits BindingList(Of T)
        Implements IBindingListView, IRaiseItemChangedEvents

        Private m_Sorted As Boolean = False
        Private m_Filtered As Boolean = False
        Private m_FilterString As String = Nothing
        Private m_SortDirection As ListSortDirection = ListSortDirection.Ascending
        Private m_SortProperty As PropertyDescriptor = Nothing
        Private m_SortDescriptions As New ListSortDescriptionCollection()
        Private m_OriginalCollection As New List(Of T)()

        Public Sub New()
            MyBase.New()
        End Sub

        Public Sub New(ByRef aList As IList(Of T))
            MyBase.New(aList)
        End Sub

        Protected Overrides ReadOnly Property SupportsSearchingCore As Boolean
            Get
                Return True
            End Get
        End Property

        Protected Overrides Function FindCore(ByVal prop As System.ComponentModel.PropertyDescriptor, ByVal key As Object) As Integer
            For i As Integer = 0 To Count - 1
                Dim item As T = Me(i)
                If prop.GetValue(item).Equals(key) Then
                    Return i
                End If
            Next
            Return -1
        End Function

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

        Protected Overrides ReadOnly Property IsSortedCore As Boolean
            Get
                Return m_Sorted
            End Get
        End Property

        Protected Overrides ReadOnly Property SortDirectionCore As ListSortDirection
            Get
                Return m_SortDirection
            End Get
        End Property

        Protected Overrides ReadOnly Property SortPropertyCore As PropertyDescriptor
            Get
                Return m_SortProperty
            End Get
        End Property

        Protected Overrides Sub ApplySortCore(ByVal prop As System.ComponentModel.PropertyDescriptor, ByVal direction As System.ComponentModel.ListSortDirection)
            m_SortDirection = direction
            m_SortProperty = prop
            Dim comparer As SortComparer(Of T) = New SortComparer(Of T)(prop, direction)
            ApplySortInternal(comparer)
        End Sub

        Private Sub ApplySortInternal(ByVal comparer As SortComparer(Of T))
            If m_OriginalCollection.Count = 0 Then
                m_OriginalCollection.AddRange(Me)
            End If
            Dim listRef As List(Of T) = Me.Items
            If listRef Is Nothing Then Exit Sub
            listRef.Sort(comparer)
            m_Sorted = True
            OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
        End Sub

        Protected Overrides Sub RemoveSortCore()
            If Not m_Sorted Then Return
            Clear()
            For Each Item As T In m_OriginalCollection
                Add(Item)
            Next
            m_OriginalCollection.Clear()
            m_SortProperty = Nothing
            m_SortDescriptions = Nothing
            m_Sorted = False
        End Sub

        Public Sub ApplySort(ByVal sorts As System.ComponentModel.ListSortDescriptionCollection) Implements System.ComponentModel.IBindingListView.ApplySort
            m_SortProperty = Nothing
            m_SortDescriptions = sorts
            Dim comparer As SortComparer(Of T) = New SortComparer(Of T)(sorts)
            ApplySortInternal(comparer)
        End Sub

        Public Property Filter As String Implements System.ComponentModel.IBindingListView.Filter
            Get
                Return m_FilterString
            End Get
            Set(ByVal value As String)
                m_FilterString = value
                m_Filtered = True
                UpdateFilter()
            End Set
        End Property

        Public Sub RemoveFilter() Implements System.ComponentModel.IBindingListView.RemoveFilter
            If Not m_Filtered Then Return
            m_FilterString = Nothing
            m_Filtered = False
            m_Sorted = False
            m_SortDescriptions = Nothing
            m_SortProperty = Nothing
            Clear()
            For Each item As T In m_OriginalCollection
                Add(item)
            Next
            m_OriginalCollection.Clear()
        End Sub

        Public ReadOnly Property SortDescriptions As System.ComponentModel.ListSortDescriptionCollection Implements System.ComponentModel.IBindingListView.SortDescriptions
            Get
                Return m_SortDescriptions
            End Get
        End Property

        Public ReadOnly Property SupportsAdvancedSorting As Boolean Implements System.ComponentModel.IBindingListView.SupportsAdvancedSorting
            Get
                Return True
            End Get
        End Property

        Public ReadOnly Property SupportsFiltering As Boolean Implements System.ComponentModel.IBindingListView.SupportsFiltering
            Get
                Return True
            End Get
        End Property

        Protected Overridable Sub UpdateFilter()
            If String.IsNullOrEmpty(m_FilterString) Then
                RemoveFilter()
                Exit Sub
            End If

            Dim equalsPos As Integer = m_FilterString.IndexOf("=")
            ' Get property name
            Dim propName As String = m_FilterString.Substring(0, equalsPos).Trim()
            ' Get filter criteria
            Dim criteria As String = m_FilterString.Substring(equalsPos + 1,
               m_FilterString.Length - equalsPos - 1).Trim()
            ' Strip leading and trailing quotes
            criteria = criteria.Substring(1, criteria.Length - 2)
            ' Get a property descriptor for the filter property
            Dim propDesc As PropertyDescriptor =
               TypeDescriptor.GetProperties(GetType(T))(propName)
            If m_OriginalCollection.Count = 0 Then
                m_OriginalCollection.AddRange(Me)
            End If
            Clear()

            '
            ' This implementation simulates a LIKE '%term%' and will 
            ' find your term within the field
            '
            For Each item As T In m_OriginalCollection
                Dim value = propDesc.GetValue(item)
                ' strip newline characters as it was causing issues with contains
                Dim sval As String = value.ToString().Replace(vbCr, "").Replace(vbLf, "")
                If sval.ToString().Contains(criteria) Then
                    Add(item)
                End If
            Next

        End Sub

        Public Overloads ReadOnly Property AllowNew As Boolean
            Get
                Return CheckReadOnly()
            End Get
        End Property

        Public Overloads ReadOnly Property AllowRemove As Boolean
            Get
                Return CheckReadOnly()
            End Get
        End Property

        Private Function CheckReadOnly() As Boolean
            If m_Sorted Or m_Filtered Then
                Return False
            Else
                Return True
            End If
        End Function

        Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As T)
            For Each propDesc As PropertyDescriptor In TypeDescriptor.GetProperties(item)
                If propDesc.SupportsChangeEvents Then
                    propDesc.AddValueChanged(item, AddressOf OnItemChanged)
                End If
            Next
            MyBase.InsertItem(index, item)
        End Sub

        Protected Overrides Sub RemoveItem(ByVal index As Integer)
            Dim item As T = Items(index)
            Dim propDescs As PropertyDescriptorCollection = TypeDescriptor.GetProperties(item)
            For Each propDesc As PropertyDescriptor In propDescs
                If propDesc.SupportsChangeEvents Then
                    propDesc.RemoveValueChanged(item, AddressOf OnItemChanged)
                End If
            Next
            MyBase.RemoveItem(index)
        End Sub

        Private Sub OnItemChanged(ByVal sender As Object, ByVal args As EventArgs)
            Dim index As Integer = Items.IndexOf(DirectCast(sender, T))
            OnListChanged(New ListChangedEventArgs(
              ListChangedType.ItemChanged, index))
        End Sub

        ReadOnly Property RaisesItemChangedEvents As Boolean
            Get
                Return True
            End Get
        End Property

    End Class

    '======================================================================
    'ソート用比較処理クラス
    Public Class SortComparer(Of T)
        Implements IComparer(Of T)

        Private m_SortCollection As ListSortDescriptionCollection = Nothing
        Private m_PropDesc As PropertyDescriptor = Nothing
        Private m_Direction As ListSortDirection = ListSortDirection.Ascending

        Public Sub New(ByVal propDesc As PropertyDescriptor, ByVal direction As ListSortDirection)
            m_PropDesc = propDesc
            m_Direction = direction
        End Sub

        Public Sub New(ByVal sortCollection As ListSortDescriptionCollection)
            m_SortCollection = sortCollection
        End Sub

        Public Function Compare(ByVal x As T, ByVal y As T) As Integer Implements System.Collections.Generic.IComparer(Of T).Compare
            If m_PropDesc IsNot Nothing Then
                Dim xValue = m_PropDesc.GetValue(x)
                Dim yValue = m_PropDesc.GetValue(y)
                Return CompareValues(xValue, yValue, m_Direction)
            ElseIf m_SortCollection IsNot Nothing AndAlso m_SortCollection.Count > 0 Then
                Return RecursiveCompareInternal(x, y, 0)
            Else
                Return 0
            End If
        End Function

        Private Function CompareValues(ByVal xValue As Object, ByVal yValue As Object, ByVal direction As ListSortDirection)
            Dim retValue As Integer = 0
            If TypeOf xValue Is IComparable Then
                retValue = DirectCast(xValue, IComparable).CompareTo(yValue)
            ElseIf TypeOf yValue Is IComparable Then
                retValue = DirectCast(yValue, IComparable).CompareTo(xValue)
            ElseIf Not xValue.Equals(yValue) Then
                retValue = xValue.ToString().CompareTo(yValue.ToString())
            End If
            If direction = ListSortDirection.Ascending Then
                Return retValue
            Else
                Return retValue * -1
            End If
        End Function

        Private Function RecursiveCompareInternal(ByVal x As T, ByVal y As T, ByVal index As Integer) As Integer
            If index >= m_SortCollection.Count Then Return 0

            Dim listSortDesc As ListSortDescription = m_SortCollection(index)
            Dim xValue = listSortDesc.PropertyDescriptor.GetValue(x)
            Dim yValue = listSortDesc.PropertyDescriptor.GetValue(y)

            Dim retValue As Integer = CompareValues(xValue, yValue, listSortDesc.SortDirection)
            If retValue = 0 Then
                index += 1
                Return RecursiveCompareInternal(x, y, index)
            Else
                Return retValue
            End If
        End Function

    End Class

End Namespace