Reputation: 411
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!!!
Upvotes: 0
Views: 928
Reputation: 411
@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.
Upvotes: 0
Reputation: 9981
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
Upvotes: 1