Observe PropertyChanged on items in a collection - c#

I'm trying to hook into an event on INotifyPropertyChanged objects in a collection.
Every answer that I've ever seen to this question has said to handle it as follows:
void NotifyingItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if( e.NewItems != null )
{
foreach( INotifyPropertyChanged item in e.NewItems )
{
item.PropertyChanged += new PropertyChangedEventHandler(CollectionItemChanged);
}
}
if( e.OldItems != null )
{
foreach( ValidationMessageCollection item in e.OldItems )
{
item.PropertyChanged -= CollectionItemChanged;
}
}
}
My problem is that this completely fails whenever a developer calls Clear() on the NotifyingItems collection. When that happens, this event handler is called with e.Action == Reset and both e.NewItems and e.OldItems equal to null (I would expect the latter to contain all items).
The problem is those items don't go away, and they aren't destroyed, they are just no longer supposed to be monitored by the current class - but since I never got the chance to unmap their PropertyChangedEventHandler - they keep calling my CollectionItemChanged handler even after they've been cleared from my NotifyingItems list. How is such a situation supposed to be handled with this 'well established' pattern?

Perhaps take a look at this answer
It suggests not using .Clear() and implementing a .RemoveAll() extension method that will remove the items one-by-one
public static void RemoveAll(this IList list)
{
while (list.Count > 0)
{
list.RemoveAt(list.Count - 1);
}
}
If that doesn't work for you, there are other good solutions posted in the link as well.

Edit: This solution doesn't work
This solution from the question Rachel linked to appears to be brilliant:
If I replace my NotifyingItems ObservableCollection with an inheriting class that overrides the overrideable Collection.ClearItems() method, then I can intercept the NotifyCollectionChangedEventArgs and replace it with a Remove instead of a Reset operation, and pass the list of removed items:
//Makes sure on a clear, the list of removed items is actually included.
protected override void ClearItems()
{
if( this.Count == 0 ) return;
List<T> removed = new List<T>(this);
base.ClearItems();
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
//If the action is a reset (from calling base.Clear()) our overriding Clear() will call OnCollectionChanged, but properly.
if( e.Action != NotifyCollectionChangedAction.Reset )
base.OnCollectionChanged(e);
}
Brilliant, and nothing needs to be changed anywhere except in my own class.
*edit*
I loved this solution, but it doesn't work... You're not allowed to raise a NotifyCollectionChangedEventArgs that has more than one item changed unless the action is "Reset". You get the following runtime exception: Range actions are not supported. I don't know why it has to be so damn picky about this, but now this leaves no option but to remove each item one at a time... firing a new CollectionChanged event for each one. What a damn hassle.

Ultimate solution discovered
I've found a solution that allows the user to both capitalize on the efficiency of adding or removing many items at a time while only firing one event - and satisfy the needs of UIElements to get the Action.Reset event args while all other users would like a list of elements added and removed.
This solution involves overriding the CollectionChanged event. When we go to fire this event, we can actually look at the target of each registered handler and determine their type. Since only ICollectionView classes require NotifyCollectionChangedAction.Reset args when more than one item changes, we can single them out, and give everyone else proper event args that contain the full list of items removed or added. Below is the implementation.
public class BaseObservableCollection<T> : ObservableCollection<T>
{
//Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
private bool _SuppressCollectionChanged = false;
/// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args.
public override event NotifyCollectionChangedEventHandler CollectionChanged;
public BaseObservableCollection() : base(){}
public BaseObservableCollection(IEnumerable<T> data) : base(data){}
#region Event Handlers
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if( !_SuppressCollectionChanged )
{
base.OnCollectionChanged(e);
if( CollectionChanged != null )
CollectionChanged.Invoke(this, e);
}
}
//CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than
//one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable
//for applications in code, so we actually check the type we're notifying on and pass a customized event args.
protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
if( handlers != null )
foreach( NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList() )
handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion
#region Extended Collection Methods
protected override void ClearItems()
{
if( this.Count == 0 ) return;
List<T> removed = new List<T>(this);
_SuppressCollectionChanged = true;
base.ClearItems();
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
public void Add(IEnumerable<T> toAdd)
{
if( this == toAdd )
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
_SuppressCollectionChanged = true;
foreach( T item in toAdd )
Add(item);
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd)));
}
public void Remove(IEnumerable<T> toRemove)
{
if( this == toRemove )
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
_SuppressCollectionChanged = true;
foreach( T item in toRemove )
Remove(item);
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove)));
}
#endregion
}
Thanks to everyone for their suggestions and links. I never would have gotten to this point without seeing all the incrementally better solutions other people came up with.

I solved this problem by making my own subclass of ObservableCollection<T> which overrides the ClearItems method. Before calling the base implementation, it raises a CollectionChanging event which I defined on my class.
CollectionChanging fires before the collection actually gets cleared, and thus you have the opportunity to subscribe to the event and unsubscribe from the events.
Example:
public event NotifyCollectionChangedEventHandler CollectionChanging;
protected override void ClearItems()
{
if (this.Items.Count > 0)
{
this.OnCollectionChanging(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
base.ClearItems();
}
protected virtual void OnCollectionChanging(NotifyCollectionChangedEventArgs eventArgs)
{
if (this.CollectionChanging != null)
{
this.CollectionChanging(this, eventArgs);
}
}

Reset does not provide the changed items. You would need to maintain a seperate collection to clear the events if you continued to use Clear.
A easier and more memory efficient solution would be to create your own clear function and remove each item instead of calling the collection's clear.
void ClearCollection()
{
while(collection.Count > 0)
{
// Could handle the event here...
// collection[0].PropertyChanged -= CollectionItemChanged;
collection.RemoveAt(collection.Count -1);
}
}

Related

How to prevent Item to be added to DataGrid?

My Problem
I am trying to prevent the user to add empty DataGrid rows when using the built-in .NET DataGrid AddNewItem-functionality. So when a user tries to commit an AddNew transaction of the DataGrid and leaves PageItemViewModel.Text empty, it should disappears from the DataGrid.
The code
ViewModels
public class PageItemViewModel
{
public string Text { get; set; }
}
public class PageViewModel
{
public ObservableCollection<PageItemViewModel> PageItems { get; } = new ObservableCollection<PageItemViewModel>();
}
View
<DataGrid AutoGenerateColumns="True"
CanUserAddRows="True"
ItemsSource="{Binding PageItems}" />
I already tried
... removing the automatically created object from the DataGrid's ItemsSource while handling:
DataGrid.AddingNewItem
INotifyCollectionChanged.CollectionChanged of PageViewModel.PageItems
IEditableCollectionView.CancelNew
DataGrid.OnItemsChanged
... but always receive exceptions like:
"System.InvalidOperationException: 'Removing' is not allowed during an AddNew or EditItem transaction."
"System.InvalidOperationException: Cannot change ObservableCollection during a CollectionChanged event."
"System.InvalidOperationException: Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead."
My question
How can I prevent the newly created PageItemViewModel from being added to the
ObservableCollection<PageItemViewModel>, when there is a given condition (in this case: String.IsNullOrWhiteSpace(PageItemViewModel.Text) == true.
EDIT:
#picnic8: The AddingNewItem event does not provide any form of RoutedEventArgs and therefore no Handled property. Instead, it is AddingNewItemEventArgs. Your code is invalid.
private void DataGrid_AddingNewItem(object sender, AddingNewItemEventArgs e)
{
var viewModel = (PageItemViewModel)e.NewItem;
bool cancelAddingNewItem = String.IsNullOrWhiteSpace(viewModel.Text) == true;
// ??? How can i actually stop it from here?
}
You can't and shouldn't prevent adding to the underlying collection because when the end user starts typing in the new row, DataGrid will create and add a new PageItemViewModel object which at that time is initialized with default values.
What you can do though is to prevent committing that object by handling the DataGrid.RowEditEnding event when DataGridRowEditEndingEventArgs.EditAction is DataGridEditAction.Commit and use DataGrid.CancelEdit method to effectively remove the new object (or restore the existing object state) when the validation failed.
private void DataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (e.EditAction == DataGridEditAction.Commit)
{
var bindingGroup = e.Row.BindingGroup;
if (bindingGroup != null && bindingGroup.CommitEdit())
{
var item = (PageItemViewModel)e.Row.Item;
if (string.IsNullOrWhiteSpace(item.Text))
{
e.Cancel = true;
((DataGrid)sender).CancelEdit();
}
}
}
}
An important detail is that the RowEditEnding event is fired before pushing the current editor value to the data source, so you need to do that by hand before performing the validation. I've used BindingGroup.CommitEdit method for that.
In your VM, subscribe to the AddingNewItem event and check your condition there. You can stop the action if the condition fails.
var datagrid.AddingNewItem += HandleOnAddingNewItem;
public void HandleOnAddingNewItem(object sender, RoutedEventArgs e)
{
if(myConditionIsTrue)
{
e.Handled = true; // This will stop the bubbling/tunneling of the event
}
}

How to fluidly reorder a collection in a listbox?

The situation is like this: I have an Observable Collection that has a bunch of objects in it which I then display to a listbox using a binding. Then periodically, I get a message from an external server that gives me a new (or possibly the same) ordering for these objects. Right now, I just clear the observable collection, and add each item back in using the specified ordering.
This doesn't look too nice. Is there a better way to go about doing this? I'd be really awesome if I could somehow get the listbox to reorder and have a nice reordering animation with it, but that might be asking too much.
I thought about adding the ordering as an attribute to each object in the ObservableCollection and then calling a sort on it. Would this look clean? My assumption is that it would be almost the same effect as clearing it and readding everything.
Thanks for any help!
I created a class that inherits from ObservableCollection. This class has a SetItems method where you need to pass in the newly ordered items. Key in this class is that it will suppress the collectionchanged event and thus will not refresh the listbox each time an item is added. It looks better and loads a lot faster.
public class SuperObservableCollection<T> : ObservableCollection<T>
{
public void SetItems(IEnumerable<T> items)
{
suppressOnCollectionChanged = true;
Clear();
AddRange(items);
}
private bool suppressOnCollectionChanged;
public void AddRange(IEnumerable<T> items)
{
suppressOnCollectionChanged = true;
if (items != null)
{
foreach (var item in items)
Add(item);
}
suppressOnCollectionChanged = false;
NotifyCollectionChanged();
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!suppressOnCollectionChanged)
base.OnCollectionChanged(e);
}
public void NotifyCollectionChanged()
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}

Cancel collection changed event on an observable collection

How can i cancel the collection changed event on an observable collection?
When my collection changes it invokes methods on a third party dll.These methods may or may not fail.
If they fail, i want dont want the item to be added to or removed from the collection. Looking at the name, it looks like the collection changed event is fired after something has been added or deleted, but how could i achieve my functionality?
Too late but it might help someone else :
class ExtendedObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification = false;
public bool AllowNotifications { get { return _suppressNotification; } }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
base.OnCollectionChanged(e);
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (!_suppressNotification)
base.OnPropertyChanged(e);
}
public void ActivateNotifications()
{
_suppressNotification = false;
}
public void DesactivateNotifications()
{
_suppressNotification = true;
}
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");
_suppressNotification = true;
foreach (T item in list)
{
Add(item);
}
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add));
}
}
I'll share the homely hack I came up with for my WPF/MVVM solution. In the methods I call for e.Action == NotifyCollectionChangedAction.Remove or e.Action == NotifyCollectionChangedAction.Add I check for failure and take any actions required to undo the change and set a boolean member _updateObservable to true. Since I can't modify the ObservableCollection during the change event, I have to defer it. Setting the boolean seemed like the easiest thing.
Then in the view model I have a property used for binding the selected item on the observable. I added to that property get method if (_updateObservable) UpdateObservable(); It appears the selected item bound property always fires its getter even if the item added or deleted does not directly affect the selected item. UpdateObservable() adds back or removes any items required from the collection and sets the flag to false.
You could simply override that particular method using the new command .
If you can manage to handle the event in your code, NotifyCollectionChangedEventArgs.NewItems returns an IList of the new items involved in the change. You could then remove these items from the collection if the methods in the third party dll failed.

C# - Dynamic properties and RaisePropertyChanged

I have following class which I use for radio button binding
public class RadioButtonSwitch : ViewModelBase
{
IDictionary<string, bool> _options;
public RadioButtonSwitch(IDictionary<string, bool> options)
{
this._options = options;
}
public bool this[string a]
{
get
{
return _options[a];
}
set
{
if (value)
{
var other = _options.Where(p => p.Key != a).Select(p => p.Key).ToArray();
foreach (string key in other)
_options[key] = false;
_options[a] = true;
RaisePropertyChanged("XXXX");
else
_options[a] = false;
}
}
}
XAML
<RadioButton Content="Day" IsChecked="{Binding RadioSwitch[radio1], Mode=TwoWay}" GroupName="Monthly" HorizontalAlignment="Left" VerticalAlignment="Center" />
ViewModel
RadioSwitch = new RadioButtonSwitch(
new Dictionary<string, bool> {{"radio1", true},{"radio2", false}}
);
I'm having problem with RaisePropertyChanged() in my Class. I'm not sure what value I should be putting in order to raise the change.
I tried putting:
Item[]
a
[a]
I keep getting following error:
This is so in case of any change I can handle it in my view accordingly. Please do not give me solutions for List for radio buttons etc.
The problem is that you are implementing an indexer, not an ordinary property. Although the binding subsystem supports indexers, MVVMLight and INotifyPropertyChanged do not.
If you want to use an indexer you need to:
Use a collection base class such as ObservableCollection<T>
Implement INotifiyCollectionChanged and raise that event instead
The first option isn't realistic because you are already deriving from ViewModelBase and have to continue to do that. Since implementing INotifiyCollectionChanged is a little bit of work, the easiest approach is to:
add a property to RadioButtonSwitch that is an observable collection of boolean values (ObservableCollection<bool>)
Then change you binding to add one more path element and you are done.
Edit:
Based on your comment and rereading your question, I think implementing INotifyCollectionChanged is the easiest. Here is the rewrite of your RadioButtonSwitch class which actually no longer needs to derive from the MVVMLight base class, although you still could if you wanted to.
The careful reader will notice that we use a sledgehammer and "reset" the whole collection when any element of the collection is modified. This is not just laziness; it is because the indexer uses a string index instead of an integer index and INotifyCollectionChanged doesn't support that. As a result, when anything changes we just throw up our hands and say the whole collection has changed.
public class RadioButtonSwitch : INotifyCollectionChanged
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected void RaiseCollectionChanged()
{
if (CollectionChanged != null)
CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
IDictionary<string, bool> _options;
public RadioButtonSwitch(IDictionary<string, bool> options)
{
this._options = options;
}
public bool this[string a]
{
get
{
return _options[a];
}
set
{
if (value)
{
var other = _options.Where(p => p.Key != a).Select(p => p.Key).ToArray();
foreach (string key in other)
_options[key] = false;
_options[a] = true;
RaiseCollectionChanged();
}
else
_options[a] = false;
}
}
}
GalaSoft.MvvmLight has the following code to check property name before raising PropertyChanged event.
public void VerifyPropertyName(string propertyName)
{
if (GetType().GetProperty(propertyName) == null)
throw new ArgumentException("Property not found", propertyName);
}
GetType().GetProperty("Item[]") obviously returns null.
This is why it is failing.
I think, the quickest workaround for you would be not to use ViewModelBase from this library, but implement your own version, that doesn't do this check:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
If you implement this class you will be able to run RaisePropertyChanged("Item[]").

How to get changes in ObservableCollection

public ObservableCollection<IndividualEntityCsidClidDetail> IncludedMembers { get; set; }
Let say I have a reference to IncludedMembers I want an event to occur when collection items are added/removed/edited.
handle the CollectionChanged event
//register the event so that every time when there is a change in collection CollectionChangedMethod method will be called
yourCollection.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler
(CollectionChangedMethod);
Create a method like this
private void CollectionChangedMethod(object sender, NotifyCollectionChangedEventArgs e)
{
//different kind of changes that may have occurred in collection
if(e.Action == NotifyCollectionChangedAction.Add)
{
//your code
}
if (e.Action == NotifyCollectionChangedAction.Replace)
{
//your code
}
if (e.Action == NotifyCollectionChangedAction.Remove)
{
//your code
}
if (e.Action == NotifyCollectionChangedAction.Move)
{
//your code
}
}
Just register to the collection's CollectionChanged event. It will raise events when you add or remove items or otherwise, change the contents of the collection.
If you want to receive events when properties of the items in the collection change, you'd need to make sure that the items are IObservable first then Subscribe() to the individual objects.
That is what observable collections are for.
Simply bind to the collection and you are sorted!

Categories