WPF: How do I hook into a ListView's ItemsSource CollectionChanged notification? - wpf

I have a ListView that is databound to an ObservableCollection ...
<ListView x:Name="List1" ItemsSource="{Binding MyList}" />
I can't seem to find any event that are triggered when the collection changes, so I'm thinking that somehow I need to hook into the collectionchanged notification somehow? I'm not really sure how to do that.
Basically, when the collection changes I want to do additional work beyond what the ListView already does in updating it's list.

By default the ItemsSource is of type IEnumerable. You need to first cast to a type that has access to the CollectionChanged event, then add a handler for that event.
((INotifyCollectionChanged)List1.ItemsSource).CollectionChanged +=
new NotifyCollectionChangedEventHandler(List1CollectionChanged);
public void List1CollectionChanged(Object sender, NotifyCollectionChangedEventArgs e)
{
// Your logic here
}
Note: I cast it to INotifyCollectionChanged in my example, but you can really cast it to any object that implements that. Though, as a best practice, you should cast to the most generic type that gives you access to the methods/properties/events you need. So, while you can cast it to an ObservableCollection, you don't need to. INotifyCollectionChanged contains the event you need and if you ever decide to use some other type of collection that implements it, this will continue to work, whereas casting to an ObservableCollection means that if you one day decide that you're list is now of type MyOwnTypeOfObservableCollectionNotDerivedFromObservableCollection than this will break. ;)
P.S. This should go in the xaml code-behind.

you are going to have to attach the handler to your list. Or, use a CollectionView and hook the changed event there.
in your codebehind, do like this:
MyList.CollectionChanged += new NotifyCollectionChangedEventHandler( this.MyCollectionChanged );
private void SortCollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
{
Debug.WriteLine( "Changed" );
}

An ObservableCollection{T} exposes the INotifyCollectionChanged.CollectionChanged event. When binding to an ItemsSource the data binding engine handles the propogation of changes from the source to the items control, but if you need to perform additional processing you can attach a handler to the CollectionChanged event and use the NotifyCollectionChangedEventArgs it provides.
Assuming you have a public property on your view model named MyList:
public ObservableCollection<T> MyList
{
get
{
if(_viewModelMyList == null)
{
_viewModelMyList = new ObservableCollection<T>;
_viewModelMyList.CollectionChanged += (o, e) =>
{
// code to process change event can go here
if(e.Action == NotifyCollectionChangedAction.Add)
{
}
};
}
return _viewModelMyList;
}
}
private ObservableCollection<T> _viewModelMyList;

Related

ObservableCollection doesn't sort newly added items

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).

Wpf detect when notifypropertychanged has been fired

I would like for my ui to perform some functions whenever the bound data has been modified.
Is it possible for the view to execute some code after the notifychange event has been called (due to changes in the underlying model)
If your model implements INotifyPropertyChanged, you can subscibe to PropertyChanged event of it.
model.PropertyChanged += new PropertyChangedEventHandler(Model_PropertyChanged);
void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
}
}
and in the handler you can check which property is changed and do your work accordingly

Notification of Collection Update for IDataErrorInfo Validation

I would like to notify the binding system in WPF of a change in a collection's item so that a validation through IDataErrorInfo gets reevaluated whenever an item inside a collection changes.
I have a custom list type which implements INotifyCollectionChanged (and works properly). But somehow the validation logic is never called, because (or at least I am assuming) that this notification does not reach the right place. Is this scenario even possible? What have I missed?
[Edit]
So basically the "architecture" is the following:
MVVM base class implements IDataErrorInfo and you can register DataValidators with lambdas in the derived MVVM classes, such as:
RegisterDataValidator(() => People, () => (People.Count == 0) ? "At least one person must be specified" : null);
The indexer on the base class checks the registered validator and returns the returned by it.
I have a SmartBindingList<T> where T: INotifyPropertyChange which is basically a list that when items are added to it, registers the items' PropertyChangedEvent and reacts to these events by firing the CollectionChanged event on the class itself:
private void OnSubPropertyChanged (object sender, PropertyChangedEventArgs e)
{
if (sender is T1)
{
if (CollectionChanged != null)
{
NotifyCollectionChangedEventArgs eventArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender);
CollectionChanged(this, eventArgs);
}
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(myPropertyName));
}
}
}
So all this works nicely, but when the code runs on the CollectionChanged(this, eventArgs) line, nothing happens in terms of validation. It should be wired up correctly, because when I add something to the collection, it works perfectly. What am I missing?
This is a bit of stab in the dark without some example code, but try raising your OnPropertyChanged notification for the properties that have changed. This should cause validation to be re-evaluated.

WPF: Cancel a user selection in a databound ListBox?

How do I cancel a user selection in a databound WPF ListBox? The source property is set correctly, but the ListBox selection is out of sync.
I have an MVVM app that needs to cancel a user selection in a WPF ListBox if certain validation conditions fail. Validation is triggered by a selection in the ListBox, rather than by a Submit button.
The ListBox.SelectedItem property is bound to a ViewModel.CurrentDocument property. If validation fails, the setter for the view model property exits without changing the property. So, the property to which ListBox.SelectedItem is bound doesn't get changed.
If that happens, the view model property setter does raise the PropertyChanged event before it exits, which I had assumed would be enough to reset the ListBox back to the old selection. But that's not working--the ListBox still shows the new user selection. I need to override that selection and get it back in sync with the source property.
Just in case that's not clear, here is an example: The ListBox has two items, Document1 and Document2; Document1 is selected. The user selects Document2, but Document1 fails to validate. The ViewModel.CurrentDocument property is still set to Document1, but the ListBox shows that Document2 is selected. I need to get the ListBox selection back to Document1.
Here is my ListBox Binding:
<ListBox
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
I did try using a callback from the ViewModel (as an event) to the View (which subscribes to the event), to force the SelectedItem property back to the old selection. I pass the old Document with the event, and it is the correct one (the old selection), but the ListBox selection doesn't change back.
So, how do I get the ListBox selection back in sync with the view model property to which its SelectedItem property is bound? Thanks for your help.
For future stumblers on this question, this page is what ultimately worked for me:
http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx
It's for a combobox, but works for a listbox just fine, since in MVVM you don't really care what type of control is calling the setter. The glorious secret, as the author mentions, is to actually change the underlying value and then change it back. It was also important to run this “undo” on a separate dispatcher operation.
private Person _CurrentPersonCancellable;
public Person CurrentPersonCancellable
{
get
{
Debug.WriteLine("Getting CurrentPersonCancellable.");
return _CurrentPersonCancellable;
}
set
{
// Store the current value so that we can
// change it back if needed.
var origValue = _CurrentPersonCancellable;
// If the value hasn't changed, don't do anything.
if (value == _CurrentPersonCancellable)
return;
// Note that we actually change the value for now.
// This is necessary because WPF seems to query the
// value after the change. The combo box
// likes to know that the value did change.
_CurrentPersonCancellable = value;
if (
MessageBox.Show(
"Allow change of selected item?",
"Continue",
MessageBoxButton.YesNo
) != MessageBoxResult.Yes
)
{
Debug.WriteLine("Selection Cancelled.");
// change the value back, but do so after the
// UI has finished it's current context operation.
Application.Current.Dispatcher.BeginInvoke(
new Action(() =>
{
Debug.WriteLine(
"Dispatcher BeginInvoke " +
"Setting CurrentPersonCancellable."
);
// Do this against the underlying value so
// that we don't invoke the cancellation question again.
_CurrentPersonCancellable = origValue;
OnPropertyChanged("CurrentPersonCancellable");
}),
DispatcherPriority.ContextIdle,
null
);
// Exit early.
return;
}
// Normal path. Selection applied.
// Raise PropertyChanged on the field.
Debug.WriteLine("Selection applied.");
OnPropertyChanged("CurrentPersonCancellable");
}
}
Note: The author uses ContextIdle for the DispatcherPriority for the action to undo the change. While fine, this is a lower priority than Render, which means that the change will show in the UI as the selected item momentarily changing and changing back. Using a dispatcher priority of Normal or even Send (the highest priority) preempts the display of the change. This is what I ended up doing. See here for details about the DispatcherPriority enumeration.
In .NET 4.5 they added the Delay field to the Binding. If you set the delay it will automatically wait to update so there is no need for the Dispatcher in the ViewModel. This works for validation of all Selector elements like the ListBox's and ComboBox's SelectedItem properties. The Delay is in milliseconds.
<ListBox
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, Delay=10}" />
-snip-
Well forget what I wrote above.
I just did an experiment, and indeed SelectedItem goes out of sync whenever you do anything more fancy in the setter. I guess you need to wait for the setter to return, and then change the property back in your ViewModel asynchronously.
Quick and dirty working solution (tested in my simple project) using MVVM Light helpers:
In your setter, to revert to previous value of CurrentDocument
var dp = DispatcherHelper.UIDispatcher;
if (dp != null)
dp.BeginInvoke(
(new Action(() => {
currentDocument = previousDocument;
RaisePropertyChanged("CurrentDocument");
})), DispatcherPriority.ContextIdle);
it basically queues the property change on the UI thread, ContextIdle priority will ensure it will wait for UI to be in consistent state. it Appears you cannot freely change dependency properties while inside event handlers in WPF.
Unfortunately it creates coupling between your view model and your view and it's an ugly hack.
To make DispatcherHelper.UIDispatcher work you need to do DispatcherHelper.Initialize() first.
Got it! I am going to accept majocha's answer, because his comment underneath his answer led me to the solution.
Here is wnat I did: I created a SelectionChanged event handler for the ListBox in code-behind. Yes, it's ugly, but it works. The code-behind also contains a module-level variable, m_OldSelectedIndex, which is initialized to -1. The SelectionChanged handler calls the ViewModel's Validate() method and gets a boolean back indicating whether the Document is valid. If the Document is valid, the handler sets m_OldSelectedIndex to the current ListBox.SelectedIndex and exits. If the document is invalid, the handler resets ListBox.SelectedIndex to m_OldSelectedIndex. Here is the code for the event handler:
private void OnSearchResultsBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewModel = (MainViewModel) this.DataContext;
if (viewModel.Validate() == null)
{
m_OldSelectedIndex = SearchResultsBox.SelectedIndex;
}
else
{
SearchResultsBox.SelectedIndex = m_OldSelectedIndex;
}
}
Note that there is a trick to this solution: You have to use the SelectedIndex property; it doesn't work with the SelectedItem property.
Thanks for your help majocha, and hopefully this will help somebody else down the road. Like me, six months from now, when I have forgotten this solution...
If you are serious about following MVVM and don't want any code behind, and also don't like the use of the Dispatcher, which frankly is not elegant either, the following solution works for me and is by far more elegant than most of the solutions provided here.
It is based on the notion that in code behind you are able to stop the selection using the SelectionChanged event. Well now, if this is the case, why not create a behavior for it, and associate a command with the SelectionChanged event. In the viewmodel you can then easily remember the previous selected index and the current selected index. The trick is to have binding to your viewmodel on SelectedIndex and just let that one change whenever the selection changes. But immediately after the selection really has changed, the SelectionChanged event fires which now is notified via the command to your viewmodel. Because you remember the previously selected index, you can validate it and if not correct, you move the selected index back to the original value.
The code for the behavior is as follows:
public class ListBoxSelectionChangedBehavior : Behavior<ListBox>
{
public static readonly DependencyProperty CommandProperty
= DependencyProperty.Register("Command",
typeof(ICommand),
typeof(ListBoxSelectionChangedBehavior),
new PropertyMetadata());
public static DependencyProperty CommandParameterProperty
= DependencyProperty.Register("CommandParameter",
typeof(object),
typeof(ListBoxSelectionChangedBehavior),
new PropertyMetadata(null));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
protected override void OnAttached()
{
AssociatedObject.SelectionChanged += ListBoxOnSelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= ListBoxOnSelectionChanged;
}
private void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
Command.Execute(CommandParameter);
}
}
Using it in XAML:
<ListBox x:Name="ListBox"
Margin="2,0,2,2"
ItemsSource="{Binding Taken}"
ItemContainerStyle="{StaticResource ContainerStyle}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
HorizontalContentAlignment="Stretch"
SelectedIndex="{Binding SelectedTaskIndex, Mode=TwoWay}">
<i:Interaction.Behaviors>
<b:ListBoxSelectionChangedBehavior Command="{Binding SelectionChangedCommand}"/>
</i:Interaction.Behaviors>
</ListBox>
The code that is appropriate in the viewmodel is as follows:
public int SelectedTaskIndex
{
get { return _SelectedTaskIndex; }
set { SetProperty(ref _SelectedTaskIndex, value); }
}
private void SelectionChanged()
{
if (_OldSelectedTaskIndex >= 0 && _SelectedTaskIndex != _OldSelectedTaskIndex)
{
if (Taken[_OldSelectedTaskIndex].IsDirty)
{
SelectedTaskIndex = _OldSelectedTaskIndex;
}
}
else
{
_OldSelectedTaskIndex = _SelectedTaskIndex;
}
}
public RelayCommand SelectionChangedCommand { get; private set; }
In the constructor of the viewmodel:
SelectionChangedCommand = new RelayCommand(SelectionChanged);
RelayCommand is part of MVVM light. Google it if you don't know it.
You need to refer to
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
and hence you need to reference System.Windows.Interactivity.
I came up against this recently, and came up with a solution that works well with my MVVM, without the need for and code behind.
I created a SelectedIndex property in my model and bound the listbox SelectedIndex to it.
On the View CurrentChanging event, I do my validation, if it fails, I simply use the code
e.cancel = true;
//UserView is my ICollectionView that's bound to the listbox, that is currently changing
SelectedIndex = UserView.CurrentPosition;
//Use whatever similar notification method you use
NotifyPropertyChanged("SelectedIndex");
It seems to work perfectly ATM. There may be edge cases where it doesnt, but for now, it does exactly what I want.
I had a very similar problem, the difference being that I am using ListView bound to an ICollectionView and was using IsSynchronizedWithCurrentItem rather than binding the SelectedItem property of the ListView. This worked well for me until I wanted to cancel the CurrentItemChanged event of the underlying ICollectionView, which left the ListView.SelectedItem out of sync with the ICollectionView.CurrentItem.
The underlying problem here is keeping the view in sync with the view model. Obviously cancelling a selection change request in the view model is trivial. So we really just need a more responsive view as far as I'm concerned. I'd rather avoid putting kludges into my ViewModel to work around limitations of the ListView synchronization. On the other hand I'm more than happy to add some view-specific logic to my view code-behind.
So my solution was to wire my own synchronization for the ListView selection in the code-behind. Perfectly MVVM as far as I'm concerned and more robust than the default for ListView with IsSynchronizedWithCurrentItem.
Here is my code behind ... this allows changing the current item from the ViewModel as well. If the user clicks the list view and changes the selection, it will immediately change, then change back if something down-stream cancels the change (this is my desired behavior). Note I have IsSynchronizedWithCurrentItem set to false on the ListView. Also note that I am using async/await here which plays nicely, but requires a little double-checking that when the await returns, we are still in the same data context.
void DataContextChangedHandler(object sender, DependencyPropertyChangedEventArgs e)
{
vm = DataContext as ViewModel;
if (vm != null)
vm.Items.CurrentChanged += Items_CurrentChanged;
}
private async void myListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var vm = DataContext as ViewModel; //for closure before await
if (vm != null)
{
if (myListView.SelectedIndex != vm.Items.CurrentPosition)
{
var changed = await vm.TrySetCurrentItemAsync(myListView.SelectedIndex);
if (!changed && vm == DataContext)
{
myListView.SelectedIndex = vm.Items.CurrentPosition; //reset index
}
}
}
}
void Items_CurrentChanged(object sender, EventArgs e)
{
var vm = DataContext as ViewModel;
if (vm != null)
myListView.SelectedIndex = vm.Items.CurrentPosition;
}
Then in my ViewModel class I have ICollectionView named Items and this method (a simplified version is presented).
public async Task<bool> TrySetCurrentItemAsync(int newIndex)
{
DataModels.BatchItem newCurrentItem = null;
if (newIndex >= 0 && newIndex < Items.Count)
{
newCurrentItem = Items.GetItemAt(newIndex) as DataModels.BatchItem;
}
var closingItem = Items.CurrentItem as DataModels.BatchItem;
if (closingItem != null)
{
if (newCurrentItem != null && closingItem == newCurrentItem)
return true; //no-op change complete
var closed = await closingItem.TryCloseAsync();
if (!closed)
return false; //user said don't change
}
Items.MoveCurrentTo(newCurrentItem);
return true;
}
The implementation of TryCloseAsync could use some kind of dialog service to elicit a close confirmation from the user.
Bind ListBox's property: IsEnabled="{Binding Path=Valid, Mode=OneWay}" where Valid is the view-model property with the validation algoritm. Other solutions look too far-fetched in my eyes.
When the disabled appearance is not allowed, a style could help out, but probably the disabled style is ok because changing the selection is not allowed.
Maybe in .NET version 4.5 INotifyDataErrorInfo helps, I dont'know.

Event handler that will be called when an item is added in a listbox

Is there an event handler that will be called when an item is added in a listbox in WPF?
Thanks!
The problem is that the INotifyCollectionChanged interface which contains the event handler is explicitly implemented, which means you have to first cast the ItemCollection before the event handler can be used:
public MyWindow()
{
InitializeComponent();
((INotifyCollectionChanged)mListBox.Items).CollectionChanged +=
mListBox_CollectionChanged;
}
private void mListBox_CollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// scroll the new item into view
mListBox.ScrollIntoView(e.NewItems[0]);
}
}
Ref.
Josh's advice about the observable collection should also be considered.
Take a different approach. Create an ObservableCollection (which does have such an event) and set the ItemsSource of the ListBox to this collection. In other words, in WPF you should think about the problem differently. The control isn't necessarily what is being modified ... the collection behind it is.
UPDATE
Based on your comment to Mitch's answer which indicates your binding source is actually an XML document, I suggest looking into hooking up to the XObject.Changed event of the XML document/element/etc. This will give you change information about the XML structure itself - not the ItemCollection which is an implementation detail you shouldn't need to consider. For example, ItemCollection (or any INotifyCollectionChanged) doesn't guarantee an individual event for every change. As you noted, sometimes you'll just get a generic reset notification.

Resources