ObservableCollection doesn't sort newly added items - wpf

I have the following ObservableCollection that's bound to a DataGrid:
public ObservableCollection<Message> Messages = new ObservableCollection<Message>;
XAML:
<DataGrid ItemsSource="{Binding Path=Messages}">
I sort it on startup, using default view:
ICollectionView view = CollectionViewSource.GetDefaultView(Messages);
view.SortDescriptions.Add(new SortDescription("TimeSent", ListSortDirection.Descending));
It all works fine, but the problem is that whenever I add a new message to Messages collection, it simply gets appended to the bottom of the list, and not sorted automatically.
Messages.Add(message);
Am I doing something wrong? I'm sure I could work around the problem by refreshing the view each time I add an item, but that just seems like the wrong way of doing it (not to mention performance-wise).

So I did a bit more investigating, and it turns out my problem is due to limitation of WPF datagrid. It will not automatically re-sort the collection when underlying data changes. In other words, when you first add your item, it will be sorted and placed in the correct spot, but if you change a property of the item, it will not get re-sorted. INotifyPropertyChanged has no bearing on sorting updates. It only deals with updating displayed data, but doesn't trigger sorting it. It's the CollectionChanged event that forces re-sorting, but modifying an item that's already in the collection won't trigger this particular event, and hence no sorting will be performed.
Here's another similar issue:
C# WPF Datagrid doesn't dynamically sort on data update
That user's solution was to manually call OnCollectionChanged().
In the end, I combined the answers from these two threads:
ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)
ObservableCollection and Item PropertyChanged
I also added 'smart' sorting, that only Calls OnCollectionChanged() if the property changed is the value that's being currently used in SortDescription.
public class MessageCollection : ObservableCollection<Message>
{
ICollectionView _view;
public MessageCollection()
{
_view = CollectionViewSource.GetDefaultView(this);
}
public void Sort(string propertyName, ListSortDirection sortDirection)
{
_view.SortDescriptions.Clear();
_view.SortDescriptions.Add(new SortDescription(propertyName, sortDirection));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
this.AddPropertyChanged(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
this.RemovePropertyChanged(e.OldItems);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Reset:
this.RemovePropertyChanged(e.OldItems);
this.AddPropertyChanged(e.NewItems);
break;
}
base.OnCollectionChanged(e);
}
private void AddPropertyChanged(IEnumerable items)
{
if (items != null)
{
foreach (var obj in items.OfType<INotifyPropertyChanged>())
{
obj.PropertyChanged += OnItemPropertyChanged;
}
}
}
private void RemovePropertyChanged(IEnumerable items)
{
if (items != null)
{
foreach (var obj in items.OfType<INotifyPropertyChanged>())
{
obj.PropertyChanged -= OnItemPropertyChanged;
}
}
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
bool sortedPropertyChanged = false;
foreach (SortDescription sortDescription in _view.SortDescriptions)
{
if (sortDescription.PropertyName == e.PropertyName)
sortedPropertyChanged = true;
}
if (sortedPropertyChanged)
{
NotifyCollectionChangedEventArgs arg = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace, sender, sender, this.Items.IndexOf((Message)sender));
OnCollectionChanged(arg);
}
}

My whole answer below is gibberish. As pointed out in the comments, if you bind to the collection itself, then you are implicitly binding to the default collection view. (However, as a comment at the link notes, Silverlight is an exception -- there no default collection view is created implicitly, unless the collection implements ICollectionViewFactory.)
The CollectionViewSource doesn't modify the underlying collection. To get the sorting, you'll need to bind to the view itself, eg:
<DataGrid ItemsSource="{Binding Path=CollectionViewSource.View}">
Note that, while the original collection (Messages) is untouched, your sorted view will get updated via the notification event:
If the source collection implements the INotifyCollectionChanged interface, the changes raised by the CollectionChanged event are propagated to the views.

I just found the problem, after trying to sort on another property and noticing that it happens to work. Turns out when my messages were being added to the collection the TimeSent property was being initialized to MinDate, and only then updated to actual date. So it was properly being placed at the bottom of the list. The issue is that the position wasn't getting updated when the TimeSent property was modified. Looks like I have an issue with propagation of INotifyPropertyChanged events (TimeSent resides in another object inside Message object).

Related

WPF MVVM - How to subscribe to PropertyChanged of model instance bound to Grid row?

My question is very similar to ones like MVVM in WPF - How to alert ViewModel of changes in Model... or should I? and ViewModel subscribing to Model's PropertyChanged event for a specific property. However, there's a difference.
Background Info
My model is Employee, and the property I'm interested in is DisplayLtdOccupationId.
But in my ViewModel, I don't just have an instance of Employee. Rather I have a collection of Employee objects.
ObservableCollection<Employee> EmployeeCensus
In my View, I have a DataGrid that's bound to EmployeeCensus. So I have Employee rows in the grid.
Question
What I would like is to be able to respond to PropertyChanged of the particular Employee row where the value of DisplayLtdOccupationId was changed. I want to do this with a handler method in my ViewModel.
In my handler, I would expect that the Employee object that just changed would be in the 'sender' variable. Then I would take its DisplayLtdOccupationId, do a lookup from a collection that I already have in memory in the ViewModel, and set the lookup value in a different property of that same Employee object.
More Details
Both the ViewModel and Employee Model implement INotifyPropertyChanged (via : Microsoft.Practices.Prism.ViewModel.NotificationObject).
The DataGrid displays the DisplayLtdOccupationId property as an inline dropdown list where the user can choose a different value.
Why not just do this in the Employee Model, in the Setter of DisplayLtdOccupationId? Because I don't have access to the lookup collection there.
I don't want to use a trigger in the view to initiate the handler. This was causing an issue, and is why I want to explore a solution using ViewModel and Model only.
There's more I could add, but I'm trying to keep the question brief and to the point. If more information is required, please advise.
Something like this. I imagine you were hoping for something a little more clever and less verbose, but this is what you get. Employee_PropertyChanged handles changes to properties of Employee. You could also give your Employee class a specific event that's raised on changes to its DisplayLtdOccupationId property. Then your parent viewmodel would handle that event instead of PropertyChanged. Either way works.
public class ViewModel : ViewModelBase
{
public ViewModel()
{
EmployeeCensus = new ObservableCollection<Employee>();
}
#region EmployeeCensus Property
private ObservableCollection<Employee> _employeeCensus = null;
public ObservableCollection<Employee> EmployeeCensus
{
get { return _employeeCensus; }
// Protect this so we don't have to handle the case of somebody giving us
// a whole new collection of new Employees.
protected set
{
if (value != _employeeCensus)
{
if (_employeeCensus != null)
{
_employeeCensus.CollectionChanged -= _employeeCensus_CollectionChanged;
}
_employeeCensus = value;
OnPropertyChanged(nameof(EmployeeCensus));
if (_employeeCensus != null)
{
_employeeCensus.CollectionChanged += _employeeCensus_CollectionChanged;
}
}
}
}
private void _employeeCensus_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (var item in e.OldItems.Cast<Employee>())
{
item.PropertyChanged -= Employee_PropertyChanged;
}
}
if (e.NewItems != null)
{
foreach (var item in e.NewItems.Cast<Employee>())
{
item.PropertyChanged += Employee_PropertyChanged;
}
}
}
private void Employee_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(Employee.DisplayLtdOccupationId):
// Do stuff
break;
}
}
#endregion EmployeeCensus Property
}

ListBox databinding and immutability

I'm having some problems with ListBox databinding and immutability. I have a model that provides a List of some elements and a ViewModel that takes these elements and puts them to an ObservableCollection which is bound to the ListBox.
The elements, however, are not mutable so when they change - which happens when user changes ListBox's selection or in a few other scenarios - the model fires up an event and the ViewModel retrieves a new List of new elements instances and repopulates the ObservableCollection.
This approach works quite well - despite being obviously not optimal - when user interacts with the ListBox via mouse (clicking) but fails horribly when using keyboard (tab to focus current element and then using mouse arrows or further tabbing). For some reason the ActiveSchema gets always reset to the first element of the Schemas[*].
The ActiveSchema setter gets called for the schema user switched to, then for null, and finally for the first value again. For some reason the two last events don't happen when invoked via mouse.
PS: Full code can be found here
PPS: I know I should probably rework the model so it exposes ObservableCollection that mutates but there're reasons why trashing everything and creating it from scratch is just a bit more reliable.
//ListBox's Items source is bound to:
public ObservableCollection<IPowerSchema> Schemas { get; private set; }
//ListBox's Selected item is bound to:
public IPowerSchema ActiveSchema
{
get { return Schemas.FirstOrDefault(sch => sch.IsActive); }
set { if (value != null) { pwrManager.SetPowerSchema(value); } }
}
//When model changes:
private void Model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(IPowerManager.PowerSchemas))
{
updateCurrentSchemas();
}
}
private void updateCurrentSchemas()
{
Schemas.Clear();
var currSchemas = pwrManager.PowerSchemas;
currSchemas.ForEach(sch => Schemas.Add(sch));
RaisePropertyChangedEvent(nameof(ActiveSchema));
}

How to set scroll position from view model with caliburn.micro?

I have a ListBox in my view, bound to a collection that is dynamically growing. I would like the scroll position to follow the last added item (which is appended to the bottom of the list). How can I achieve this with Caliburn.Micro?
An alternative could be to use the event aggregator to publish a message to the view.
Something like:
Aggregator.Publish(ItemAddedMessage<SomeItemType>(itemThatWasJustAdded));
and in the view:
public class SomeView : IHandle<ItemAddedMessage<SomeItemType>>
{
public void Handle(ItemAddedMessage<SomeItemType> message)
{
// Implement view specific behaviour here
}
}
It depends on what your requirements are but at least then the view is responsible for display concerns and you can still test the VM
Also you could just implement the code solely in the view - since it appears to be a view concern (e.g. using the events that listbox provides)
A behaviour would also be useful but maybe one that's a little less coupled to your types - e.g. a generic behaviour SeekAddedItemBehaviour which hooks listbox events to find the last item. Not sure if the listbox exposes the required events, but worth a look
EDIT:
Ok this may work full stop - you should be able to just attach this behaviour to the listbox and it should take care of the rest:
public class ListBoxSeekLastItemBehaviour : System.Windows.Interactivity.Behavior<ListBox>
{
private static readonly DependencyProperty ItemsSourceWatcherProperty = DependencyProperty.Register("ItemsSourceWatcher", typeof(object), typeof(ListBoxSeekLastItemBehaviour), new PropertyMetadata(null, OnItemsSourceWatcherPropertyChanged));
private ListBox _listBox = null;
private static void OnItemsSourceWatcherPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ListBoxSeekLastItemBehaviour source = d as ListBoxSeekLastItemBehaviour;
if (source != null)
source.OnItemsSourceWatcherPropertyChanged();
}
private void OnItemsSourceWatcherPropertyChanged()
{
// The itemssource has changed, check if it raises collection changed notifications
if (_listBox.ItemsSource is INotifyCollectionChanged)
{
// if it does, hook the CollectionChanged event so we can respond to items being added
(_listBox.ItemsSource as INotifyCollectionChanged).CollectionChanged += new NotifyCollectionChangedEventHandler(ListBoxSeekLastItemBehaviour_CollectionChanged);
}
}
void ListBoxSeekLastItemBehaviour_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems.Count > 0)
{
// If an item was added seek it
ScrollIntoView(e.NewItems[0]);
}
}
protected override void OnAttached()
{
base.OnAttached();
// We've been attached - get the associated listbox
var box = this.AssociatedObject as ListBox;
if (box != null)
{
// Hold a ref
_listBox = box;
// Set a binding to watch for property changes
System.Windows.Data.Binding binding = new System.Windows.Data.Binding("ItemsSource") { Source = _listBox; }
// EDIT: Potential bugfix - you probably want to check the itemssource here just
// in case the behaviour is applied after the original ItemsSource binding has been evaluated - otherwise you might miss the change
OnItemsSourceWatcherPropertyChanged();
}
}
private void ScrollIntoView(object target)
{
// Set selected item and try and scroll it into view
_listBox.SelectedItem = target;
_listBox.ScrollIntoView(target);
}
}
You probably want to tidy it up a bit and also make sure that the event handler for CollectionChanged is removed when the ItemsSource changes.
Also you might want to call it SeekLastAddedItemBehaviour or SeekLastAddedItemBehavior - I tend to keep the US spelling since it matches Microsoft's spelling. I think SeekLastItem sounds like it will scroll to the last item in the list rather than the last added item
You could reference the view in the view model using GetView(). That also couples the view and view model.
var myView = GetView() as MyView;
myView.MyListBox.DoStuff
Another option is to create a behavior. This is an example of how to use a behavior to expand a TreeView from the view model. The same could be applied to a ListBox.
Actually, there is an easier way to achieve this, without any of the above.
Just extend your Listbox with the following:
namespace Extensions.Examples {
public class ScrollingListBox : ListBox
{
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
int newItemCount = e.NewItems.Count;
if (newItemCount > 0)
this.ScrollIntoView(e.NewItems[newItemCount - 1]);
base.OnItemsChanged(e);
}
}
}
}
Then in Xaml, Declare the Location of your extension class as so:
xmlns:Extensions="clr-namespace:Extensions.Examples"
And when you create your listbox, instead of using
<Listbox></Listbox>
Just use your extended class
<Extensions:ScrollingListBox></Extensions:ScrollingListBox>

ItemsSource and collections in which only element properties change

I'm having grief with a ComboBox not reflecting changes in the properties of the collection to which its ItemsSource is bound.
There is a tree made up of a Categories observable collection of Category objects containing Setting objects. Some settings define the presentation names for the domain of values permitted for other settings. These are spread over several categories, but a little magic with LINQ produces an ObservableCollection in which ChannelCategory exposes properties ChannelNumber and ChannelTitle.
ChannelCategory.ChannelTitle is a StringSetting, ChannelNumber is an int, an immutable identity value for which ChannelTitle provides a display string. Omitting abundant but irrelevant other nodes, we have the following:
Channels
ChannelCategory
ChannelNumber = 1
ChannelTitle = "caption for channel one"
ChannelCategory
ChannelNumber = 2
ChannelTitle = "caption for channel two"
ChannelCategory
ChannelNumber = 3
ChannelTitle = "caption for channel three"
...
This Channels collection is prepared and exposed by a property on a class instantiated and added to the window resource dictionary in XAML (accessible as a StaticResource). This arrangement allows me declaratively bind a combobox to Channels
<ComboBox VerticalAlignment="Center" Grid.Column="2"
ItemsSource="{Binding Source={StaticResource cats}, Path=Channels}"
DisplayMemberPath="ChannelTitle.Editor"
SelectedValuePath="ChannelNumber"
SelectedValue="{Binding Editor}"
/>
This all works, but edits elsewhere to the ChannelTitle value are not reflected in the values shown in the combobox list.
Various debugging trickery with breakpoints and the DropDownOpened event allowed me to ascertain that the updates are available from the collection referenced by ItemsSource.
And finally we reach the mystery. Why doesn't the combobox detect changes to the elements of the collection? The collection itself is an ObservableCollection so it should notify property changes.
The elements of the collection are all ChannelCategory which is a DependencyObject, and ChannelCategory.ChannelTitle is a DependencyProperty.
I think the problem is that I'm neither adding nor removing items from the collection, so as far as the collection is concerned it has the same elements and therefore hasn't changed.
Can anyone suggest a strategy for causing changes to ChannelTitle to cause Channels collection to notify change so the combobox updates?
Rachel's suggestion ended up as shown below. In the context of my application there was considerably more complexity because each ChannelCategory owns a collection of settings objects, so the changing value is a property of a an object in a collection that is a property of an object in the collection to which ItemsSource is bound. But the essence of Rachel's suggestion simply needed application at two levels.
public class ObservableChannelCollection : ObservableCollection<ChannelCategory>
{
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (ChannelCategory channel in e.NewItems)
channel.PropertyChanged += channel_PropertyChanged;
if (e.OldItems != null)
foreach (ChannelCategory channel in e.OldItems)
channel.PropertyChanged -= channel_PropertyChanged;
base.OnCollectionChanged(e);
}
void channel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(e);
}
}
ObservableCollection tracks changes to the Collection itself, such as Add, Remove, Reset, etc, but it does not track changes to individual items inside the collection. So if you update the property on an item in an ObservableCollection, the collection doesn't get notified that something has changed.
One alternative is to add an event to the ObservableCollection.CollectionChanged event, and when new items get added, hook up a property change on the new items that will raise the collection changed event.
void MyObservableCollection_CollectionChanged(object sender, CollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach(MyItem item in e.NewItems)
{
MyItem.PropertyChanged += MyItem_PropertyChanged;
}
}
if (e.OldItems!= null)
{
foreach(MyItem item in e.OldItems)
{
MyItem.PropertyChanged -= MyItem_PropertyChanged;
}
}
}
void MyItem_PropertyChanged(object sender, PropertyChange e)
{
RaisePropertyChanged("MyObservableCollection");
}

Collection element behavior state is not shown after the list binding source changes

I'm quite new at WPF and recently encountered a problem. I got an ObservableCollection< TankCar> TankCars in my ViewModel which is the Binding Source for a ListBox in the view. The ListBox has ItemTemplate that defines two behaviors:
<i:Interaction.Behaviors>
<ei:DataStateBehavior x:Name="Type62DataStateBehavior" Binding="{Binding Type}" Value="62" TrueState="IsType62"/>
<ei:DataStateBehavior x:Name="Type66DataStateBehavior" Binding="{Binding Type}" Value="66" TrueState="IsType66"/>
</i:Interaction.Behaviors>
When I add items to the collection, I can see them appear in the list. But the state is not shown until I call TankCars[i].RaisePropertyChanged("Type")
Moreover, when I need to switch to another collection, I call the code:
TankCars = new ObservableCollection<TankCar> (GetTankCars());
RaisePropertyChanged("TankCars"); //to notify the ListBox
foreach (var car in TankCars) {car.RaisePropertyChanged("Type");} //make states change (not working)
and it appears that after I change ItemSource binding through raising TankCars property change event, items states are not shown (TankCar PropertyChangedEvent is not bound to anything at the moment). If I place a button on a form, that launches the command that calls car.RaisePropertyChanged("Type") for the items, it refreshes the items states.
So the question is: how to make things right to make the behavior of the items trigger after adding new items to a collection and after replacing it by another one? And why the items states are not refreshed, when I raise PropertyChanged just after the changing ListBox item source?
Update: solved with the code below (also helped this answer). And no more manual raising PropertyChanged for collection items, I'm happy :)
public class SmartDataStateBehavior : DataStateBehavior
{
protected override void OnAttached()
{
base.OnAttached();
if (AssociatedObject != null)
{
AssociatedObject.Loaded += AssociatedObjectLoaded;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject != null)
{
AssociatedObject.Loaded -= AssociatedObjectLoaded;
}
}
private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
{
if (Binding == null || Value == null) return;
if (Binding.ToString() == Value.ToString())
{
if (TrueState != null)
VisualStateManager.GoToElementState(AssociatedObject, TrueState, true);
}
else
{
if (FalseState != null)
VisualStateManager.GoToElementState(AssociatedObject, FalseState, true);
}
}
}
When you have just added the items the respective containers with their behaviours have not yet been created and hence the type is not bound yet, making the notification meaningless.
You normally should not need to raise notifications outside the setter of a property, so i would try to avoid doing all that manually.
I do not know what your behavior does but if it affects the type somehow you should put that notification logic either into the setter of type if it has one or into the OnAttached override method of the bahaviour.

Resources