Вопрос

Experts,

Short Version:

The way ObservableCollection sorts is the most recent at the end, and I need it to be exactly the opposite, for display in a WPF DataGrid. Right now, all is well, but new entries are added to the end, so the user can't see the new rows added.

Less Short Version:

I have a DateTime field on the Entry class if needed to sort by/on, but to be honest, if I could just add them to the top as I get them, I don't even need to sort! I just need:

*Each item added to the collection to be added to the top, and not the default bottom.*

A Little Longer Version:

I simply cannot find a way to introduce new elements to the 'top' of the collection... "Why?" Well, I am displaying rows of data in a WPF form, and I want the most current on the top, sorted by a date field in the object.

If it is the same as IList, then why is this so hard?

Too complicated? Let me simplify:

Really Long Version:

At the very start, there is a class that will make up a row in a WPF DataGrid. The class is called "Entry", and the only 2 properties that matter below are:

Class Entry
[...]
Public Property TsCreated As Nullable(Of DateTime)
Public Property EntryRaw As String
    Set(value As String)
        If value <> _entryRaw Then
            _entryRaw = value
            OnPropertychanged("EntryRaw")
        End If
    End Set
    Get
        Return _entryRaw
    End Get
End Property
Private _entryRaw As String
[...]
End Class

Next is the ObservableCollection of these Entrys...

Public Class SysEvents
    Inherits ObservableCollection(**Of Entry**)

    Private Shared _list As New SysEvents

    Public Sub New()
    End Sub
End Class

ObservableCollection has a non-overridable Sub Add(), so I can't write my own Sub Add() and sort right after adding...

So in use, this WPF Window class is like this (again, making it really simple):

Class MainWindow

    Private RawEvents As New toag.syslog.SysEvents

    Sub New()
        grdEntryRaw.ItemsSource = RawEvents ' grid that holds rows
    End Sub

    Sub AddRow()
        Me.RawEvents.Add(sysEvent)
    End Sub

End Class

As if it matters, the XAML (can I sort in XAML?):

<DataGrid x:Name="grdEntryRaw" [...]>
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding EntryRaw}" Header="Entry Raw" />
    </DataGrid.Columns>
</DataGrid>

OK. Nobody Made It This Far Version:

Since I can't intercept the .Add() that the Binding is doing, it seems like I can't ever get in there with some sorting algorithm...

I thought the fight was over when I figured this much out... But now it seems success has been snatched from me on the 1 yard line! Oh, Visual Studio.. .you are a cruel mistress...

TIA!!!

Это было полезно?

Решение

There's nothing special about the ObservableCollection other than it implements INotifyCollectionChanged and INotifyPropertyChanged.

I suggests that you create your own ObservableCollection with the required behavior.

Public Class ObservableStack(Of T)
    Implements IEnumerable, ICollection, IList
    Implements IEnumerable(Of T), ICollection(Of T), IList(Of T)
    Implements INotifyCollectionChanged, INotifyPropertyChanged

    Public Sub New()
        Me.list = New List(Of T)
    End Sub

   '...

    Public Sub Add(item As T) Implements ICollection(Of T).Add
        'TODO: Validate.
        Me.list.Insert(0, item) 'Insert at top of the list.
        Me.RaisePropertyChanged("Count")
        Me.RaisePropertyChanged("Item")
        Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Add, item, 0)
    End Sub

    Private Function _Add(obj As Object) As Integer Implements IList.Add
        Me.Add(TryCast(obj, T))
        Return 0
    End Function

   '...

    Private ReadOnly list As List(Of T)

End Class

Example

Public Class ObservableStack(Of T)
    Implements IEnumerable, ICollection, IList
    Implements IEnumerable(Of T), ICollection(Of T), IList(Of T)
    Implements INotifyCollectionChanged, INotifyPropertyChanged

    Public Sub New()
        Me.list = New List(Of T)
    End Sub

    Public Event CollectionChanged As NotifyCollectionChangedEventHandler Implements INotifyCollectionChanged.CollectionChanged
    Protected Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public ReadOnly Property Count() As Integer Implements ICollection.Count, ICollection(Of T).Count
        Get
            Return Me.list.Count
        End Get
    End Property

    Default Public Property Item(index As Integer) As T Implements IList(Of T).Item
        Get
            Return Me.list.Item(index)
        End Get
        Set(value As T)
            Me.Replace(index, value)
        End Set
    End Property

    Private ReadOnly Property _IsFixedSize() As Boolean Implements IList.IsFixedSize
        Get
            Return CType(Me.list, IList).IsFixedSize
        End Get
    End Property

    Private ReadOnly Property _IsReadOnly() As Boolean Implements IList.IsReadOnly, ICollection(Of T).IsReadOnly
        Get
            Return CType(Me.list, IList).IsReadOnly
        End Get
    End Property

    Private ReadOnly Property _IsSynchronized() As Boolean Implements ICollection.IsSynchronized
        Get
            Return CType(Me.list, ICollection).IsSynchronized
        End Get
    End Property

    Private Property _Item(index As Integer) As Object Implements IList.Item
        Get
            Return Me.Item(index)
        End Get
        Set(value As Object)
            Me.Item(index) = DirectCast(value, T)
        End Set
    End Property

    Private ReadOnly Property _SyncRoot() As Object Implements ICollection.SyncRoot
        Get
            Return CType(Me.list, ICollection).SyncRoot
        End Get
    End Property

    Public Sub Add(item As T) Implements ICollection(Of T).Add
        Me.Insert(0, item)
    End Sub

    Public Sub Clear() Implements IList.Clear, ICollection(Of T).Clear
        If (Me.Count > 0) Then
            Me.list.Clear()
            Me.RaisePropertyChanged("Count")
            Me.RaisePropertyChanged("Item")
            Me.RaiseCollectionReset()
        End If
    End Sub

    Public Function Contains(item As T) As Boolean Implements ICollection(Of T).Contains
        Return Me.list.Contains(item)
    End Function

    Public Sub CopyTo(array() As T, index As Integer) Implements ICollection(Of T).CopyTo
        Me.list.CopyTo(array, index)
    End Sub

    Public Function GetEnumerator() As IEnumerator(Of T) Implements IEnumerable(Of T).GetEnumerator
        Return Me.list.GetEnumerator()
    End Function

    Public Function IndexOf(item As T) As Integer Implements IList(Of T).IndexOf
        Return Me.list.IndexOf(item)
    End Function

    Public Sub Insert(index As Integer, item As T) Implements IList(Of T).Insert
        'TODO: Validate item.
        Me.list.Insert(index, item)
        Me.RaisePropertyChanged("Count")
        Me.RaisePropertyChanged("Item")
        Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Add, item, index)
    End Sub

    Public Sub Move(ByVal oldIndex As Integer, ByVal newIndex As Integer)
        Me.MoveItem(oldIndex, newIndex)
    End Sub

    Protected Overridable Sub MoveItem(ByVal oldIndex As Integer, ByVal newIndex As Integer)
        Dim item As T = Me.Item(oldIndex)
        Me.list.RemoveAt(oldIndex)
        Me.list.Insert(newIndex, item)
        Me.RaisePropertyChanged("Item")
        Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex)
    End Sub

    Protected Overridable Sub OnCollectionChanged(e As NotifyCollectionChangedEventArgs)
        RaiseEvent CollectionChanged(Me, e)
    End Sub

    Protected Overridable Sub OnPropertyChanged(e As PropertyChangedEventArgs)
        RaiseEvent PropertyChanged(Me, e)
    End Sub

    Private Sub RaiseCollectionChanged(action As NotifyCollectionChangedAction, item As T, index As Integer)
        Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(action, item, index))
    End Sub

    Private Sub RaiseCollectionChanged(ByVal action As NotifyCollectionChangedAction, ByVal item As Object, ByVal index As Integer, ByVal oldIndex As Integer)
        Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(action, item, index, oldIndex))
    End Sub

    Private Sub RaiseCollectionChanged(action As NotifyCollectionChangedAction, oldItem As T, newItem As T, index As Integer)
        Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(action, newItem, oldItem, index))
    End Sub

    Private Sub RaiseCollectionReset()
        Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
    End Sub

    Private Sub RaisePropertyChanged(propertyName As String)
        Me.OnPropertyChanged(New PropertyChangedEventArgs(propertyName))
    End Sub

    Public Function Remove(item As T) As Boolean Implements ICollection(Of T).Remove
        Dim index As Integer = Me.IndexOf(item)
        If (index <> -1) Then
            Me.RemoveAt(index)
            Return True
        End If
        Return False
    End Function

    Public Sub RemoveAt(index As Integer) Implements IList.RemoveAt, IList(Of T).RemoveAt
        Dim item As T = Me.Item(index)
        Me.list.RemoveAt(index)
        Me.RaisePropertyChanged("Count")
        Me.RaisePropertyChanged("Item")
        Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Remove, item, index)
    End Sub

    Public Sub Replace(index As Integer, newItem As T)
        'TODO: Validate item.
        Dim oldItem As T = Me.Item(index)
        Me.list.Item(index) = newItem
        Me.RaisePropertyChanged("Item")
        Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Replace, oldItem, newItem, index)
    End Sub

    Private Function _Add(obj As Object) As Integer Implements IList.Add
        Me.Add(DirectCast(obj, T))
        Return 0
    End Function

    Private Function _Contains(obj As Object) As Boolean Implements IList.Contains
        Return Me.Contains(DirectCast(obj, T))
    End Function

    Private Sub _CopyTo(array As Array, index As Integer) Implements ICollection.CopyTo
        CType(Me.list, ICollection).CopyTo(array, index)
    End Sub

    Private Function _GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
        Return Me.GetEnumerator()
    End Function

    Private Function _IndexOf(obj As Object) As Integer Implements IList.IndexOf
        Return Me.IndexOf(DirectCast(obj, T))
    End Function

    Private Sub _Insert(index As Integer, obj As Object) Implements IList.Insert
        Me.Insert(index, DirectCast(obj, T))
    End Sub

    Private Sub _Remove(obj As Object) Implements IList.Remove
        Me.Remove(DirectCast(obj, T))
    End Sub

    Private ReadOnly list As List(Of T)

End Class

Другие советы

@Bjørn-Roger Kringsjå certainly pointed me in the right direction.

I stripped things down to the bare minimum.

Instead of making my own collection class, I created the collection inside the WPF Window class:

declaration at the top of the WPF MainWindow class:

Private RawEvents As ObservableCollection(Of Entry)

Then, I had to instantiate it in the mechanics of the class' instantiation, and se tthe ItemsSource for the DataGrid:

RawEvents = New ObservableCollection(Of Entry)
grdEntryRaw.ItemsSource = RawEvents ' source for main window (events)

The only thing left was to put new events into the collection (I get new events from a message queue, but it matters not:

Public Sub PeekQ(ByVal sender As System.Object, ByVal e As System.Messaging.PeekCompletedEventArgs) Handles Q.PeekCompleted
    [..]    
    ' send to main display (newest entries on top)
    Me.Dispatcher.Invoke(CType(Sub() **Me.RawEvents.Insert(0, someEvent)**, Action))
    '
    Me.CountryLookupQ.BeginPeek()

End Sub

...and that is it! I didn't even need an additional class to hold the events... I just used the ObservableCollection created inside the WPF window. The XAML is dead simple, and the best part is that there is no sorting algorithm:

[...]
<DockPanel x:Name="EntryRawDockPanel"  HorizontalAlignment="Left" LastChildFill="False" Width="517" Margin="0,26,0,41">
    <DataGrid x:Name="grdEntryRaw" Grid.Column="1" Margin="0,0,10,43" AutoGenerateColumns="False" HorizontalContentAlignment="Stretch" CanUserAddRows="False" CanUserDeleteRows="True" AlternatingRowBackground="#FFDEFFE4">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding EntryRaw}" Header="Entry Raw" IsReadOnly="False"/>
        </DataGrid.Columns>
    </DataGrid>
</DockPanel>
[...]

Honestly, that is the entire solution. The Entry() class is not special in any way.

Hope this helps someone else, and yes, I ave seen more than one way to do this, like sorting in XAML, and even instantiating a class in XAML, but this is the easiest for my way of writing.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top