Custom Observable Collection with AddRange & NotifyCollectionChangedAction.Add not working - c#

I have implemented an ObservableCollection to support bulk addition like below and bind to a WPF UI -
public void AddRange(IEnumerable<T> list)
{
lock (_lock)
{
if (list == null)
{
throw new ArgumentNullException("list");
}
_suspendCollectionChangeNotification = true;
var newItems = new List<T>();
foreach (T item in list)
{
if (!Contains(item))
{
Add(item);
newItems.Add(item);
}
}
_suspendCollectionChangeNotification = false;
var arg = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems);
OnCollectionChanged(arg); //NotifyCollectionChangedAction.Reset works!!!
}
}
}
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChanged(e);
}
internal void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (IsCollectionChangeSuspended)
{
return;
}
NotifyCollectionChangedEventHandler handler = CollectionChanged;
if (handler != null)
{
if (Application.Current != null && !Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.DataBind,handler, this, e);
}
else
{
handler(this, e);
}
}
}
private bool IsCollectionChangeSuspended
{
get { return _suspendCollectionChangeNotification; }
}
I get this error - {"Collection was modified; enumeration operation may not execute."}
But, if I change the NotifyCollectionChangedAction.Add to NotifyCollectionChangedAction.Reset and do not pass any changed list, then it binds correctly to the UI. But, I want to use NotifyCollectionChangedAction.Add such that I can observe on the changes.
Can anyone please correct me ?

If you use the internal IList<T> to add the items it wont notify all the events like using Add method,
The Add method works like this:
CheckReentrancy
InsertItem
OnPropertyChanged("Count");
OnPropertyChanged("Item[]");
OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
So if you skip the Add method and add the items directly to the underlying collection you may get this to wok.
Example: (untested)
public void AddRange(IEnumerable<T> rangeItems)
{
foreach (var item in rangeItems)
{
Items.Add(item);
}
base.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
base.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
var arg = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, rangeItems);
OnCollectionChanged(arg);
}

Related

c# wpf MVVM Get ObservableList from ObservableDictionary

i had some problems with the speed of my application and done some performance profiling. The result shows that there is a lot of time in my application that is spended with linq querys especialy to the ID's of the Models. My idea was to create a observable dictionary with the ID as the key and the model as the Value. This works pretty good and is much faster than the linq querys with
.Any(x => x.ID == id)
or the linq query with
.First(x => x.ID == id)
As ObservableDictionary i used this sample
http://blogs.microsoft.co.il/shimmy/2010/12/26/observabledictionarylttkey-tvaluegt-c/
the problem now is that i need to create a ObservableCollection wich i can bind to my Views. I tried to expand the ObservableDictionary with a ObservableValue Property but that does not work
public ObservableCollection<TValue> ObservableValues
{
get
{
if (observableValues == null)
{
lock (lockObject)
{
if (observableValues == null)
observableValues = new ObservableCollection<TValue>(Dictionary.Values);
}
}
return observableValues;
}
}
When i add a Model to my dictionary or update a model the ObservableCollection which is bound to the Views will not update.
The best solution I can think of is to keep in your ObservableDictionnary an ObservableCollection containing all the values of the dictionnary.
You then have to change your class so that when you insert/update/delete a value in the dicionnary, it does the same in the ObservableCollection thus, firing the event to update the view.
Thanks for the suggestion but i tryed some implementations of a KeyedCollection and decided to switch all Collections / Dictionarys to my custom implementation of a KeyedCollection. The Performance is as fast as with the dictionary but without the using of the KVP's. Heres my observable implementation with some Replace Methods and it worked very fast.
public class ObservableKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem>, INotifyCollectionChanged
{
private const string CountString = "Count";
private readonly Func<TItem, TKey> _getKeyForItemDelegate;
// Constructor now requires a delegate to get the key from the item
public ObservableKeyedCollection(Func<TItem, TKey> getKeyForItemDelegate) : base()
{
if (getKeyForItemDelegate == null)
throw new ArgumentNullException("Delegate passed can't be null!");
_getKeyForItemDelegate = getKeyForItemDelegate;
}
protected override TKey GetKeyForItem(TItem item)
{
return _getKeyForItemDelegate(item);
}
/// <summary>
/// Method to add a new object to the collection, or to replace an existing one if there is
/// already an object with the same key in the collection.
/// </summary>
public void AddOrReplace(TItem newObject)
{
int i = GetItemIndex(newObject);
if (i != -1)
SetItem(i, newObject);
else
Add(newObject);
}
/// <summary>
/// Method to replace an existing object in the collection, i.e., an object with the same key.
/// An exception is thrown if there is no existing object with the same key.
/// </summary>
public void Replace(TItem newObject)
{
int i = GetItemIndex(newObject);
if (i != -1)
SetItem(i, newObject);
else
throw new Exception("Object to be replaced not found in collection.");
}
/// <summary>
/// Method to get the index into the List{} in the base collection for an item that may or may
/// not be in the collection. Returns -1 if not found.
/// </summary>
private int GetItemIndex(TItem itemToFind)
{
TKey keyToFind = GetKeyForItem(itemToFind);
if (this.Contains(keyToFind))
return this.IndexOf(this[keyToFind]);
else return -1;
}
// Overrides a lot of methods that can cause collection change
protected override void SetItem(int index, TItem item)
{
var oldItem = base[index];
base.SetItem(index, item);
OnCollectionChanged(NotifyCollectionChangedAction.Replace, item, oldItem);
}
protected override void InsertItem(int index, TItem item)
{
base.InsertItem(index, item);
OnCollectionChanged(NotifyCollectionChangedAction.Add, item);
}
protected override void ClearItems()
{
base.ClearItems();
OnCollectionChanged();
}
protected override void RemoveItem(int index)
{
TItem item = this[index];
base.RemoveItem(index);
OnCollectionChanged(NotifyCollectionChangedAction.Remove, item);
}
private bool _deferNotifyCollectionChanged = false;
public void AddRange(IEnumerable<TItem> items)
{
_deferNotifyCollectionChanged = true;
foreach (var item in items)
Add(item);
_deferNotifyCollectionChanged = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (_deferNotifyCollectionChanged)
return;
if (CollectionChanged != null)
CollectionChanged(this, e);
}
#region INotifyCollectionChanged Members
public event PropertyChangedEventHandler PropertyChanged;
public event NotifyCollectionChangedEventHandler CollectionChanged;
private void OnPropertyChanged()
{
OnPropertyChanged(CountString);
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private void OnCollectionChanged()
{
if (_deferNotifyCollectionChanged)
return;
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void OnCollectionChanged(NotifyCollectionChangedAction action, TItem changedItem)
{
if (_deferNotifyCollectionChanged)
return;
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, changedItem));
}
private void OnCollectionChanged(NotifyCollectionChangedAction action, TItem newItem, TItem oldItem)
{
if (_deferNotifyCollectionChanged)
return;
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem));
}
private void OnCollectionChanged(NotifyCollectionChangedAction action, IList newItems)
{
if (_deferNotifyCollectionChanged)
return;
OnPropertyChanged();
if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItems));
}
#endregion
}

C# subscribing to event, when using generic collection of publisher

What is the best way to subscribe to an event, where the subscriber is using a generic collection of the event publishing class?
To briefly outline, the setup is something like this:
public class PublisherClass
{
public event EventHandler<CustomEventArgs> DoneSomethingEvent;
...
public class SubscriberClass
{
public List<PublisherClass> publisherClassList { get; set; }
...
Now, if I were to just use PublisherClass it would be as simple as:
publisherClass.DoneSomethingEvent += MyEventHandler;
How do I do this if I'm working with a generic collection of this class?
You would need to put a constraint on the generic type and that constraint would need to be a class or interface that defines the event you are wanting to subscribe to.
public interface IDoSomthingPublisher
{
event EventHandler<CustomEventArgs> DoneSomethingEvent;
}
public class PublisherClass : IDoSomthingPublisher
{
public event EventHandler<CustomEventArgs> DoneSomethingEvent;
...
}
public class SubscriberClass<T> where T : IDoSomthingPublisher
{
private List<T> _publisherClassList;
public List<T> PublisherClassList
{
get
{
return _publisherClassList;
}
set
{
if(_publisherClassList != null)
{
foreach(T item in _publisherClassList)
{
//unsubscribe old events
item -= MyEventHandler;
}
}
_publisherClassList = value;
if(_publisherClassList != null)
{
foreach(T item in _publisherClassList)
{
//subscribe to new events
item += MyEventHandler;
}
}
}
}
}
You might want to swap out the List<T> to a ObserveableCollection<T> so you can get notified if a item is added or removed from the list from outside of your class.
public class SubscriberClass<T> where T : IDoSomthingPublisher
{
private ObserveableCollection<T> _publisherClassList;
public ObserveableCollection<T> PublisherClassList
{
get
{
return _publisherClassList;
}
set
{
if(_publisherClassList != null)
{
_publisherClassList -= CollectionChangedHandler;
foreach(T item in _publisherClassList)
{
//unsubscribe old events
item -= MyEventHandler;
}
}
_publisherClassList = value;
if(_publisherClassList != null)
{
_publisherClassList += CollectionChangedHandler;
foreach(T item in _publisherClassList)
{
//subscribe to new events
item += MyEventHandler;
}
}
}
}
private void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs args)
{
if(args.Action == NotifyCollectionChangedAction.Move)
return; //Don't need to change subscriptions for moves.
if(args.Action == NotifyCollectionChangedAction.Reset)
throw new NotImplementedException("I leave this to you to solve");
foreach(var item in args.OldItems)
{
item -= MyEventHandler;
}
foreach(var item in args.NewItems)
{
item += MyEventHandler;
}
}
}

WPF How to raise an Individual property change from DataGrid bounded to a ObservableCollection

As the question ask, how is this done ?
From a DataGrid I can get the SelectedRow of that collection that has been bounded to its ItemSource, but I really need a setter and getter for an individual property that belongs inside the ObservableCollection.
For example, I need to catch when a user checks a bool property inside a datagrid, so then the setter would be set to "false/true". So something like
//But the Archive property is in the DataContext of the row item...
//so this wouldnt work, I think..
private bool m_Archived = false;
public bool Archived
{
get { return m_Archived; }
set
{
m_Archived = value;
OnPropertyChanged("Archived");
}
}
But remember this property is part of the ObservableCollection (DataContext)
Cheers
You need to register to each collection item property changed, then to control when the desired property has changed, then change the archived property. Check this sample code:
private ObservableCollection<TClass> _SomeObservableCollection;
public ObservableCollection<TClass> SomeObservableCollection
{
get { return _SomeObservableCollection ?? (_SomeObservableCollection = SomeObservableCollectionItems()); }
}
private ObservableCollection<TClass> SomeObservableCollectionItems()
{
var resultCollection = new ObservableCollection<TClass>();
foreach (var item in SomeModelCollection)
{
var newPoint = new TClass(item) {IsLocated = true};
newPoint.PropertyChanged += OnItemPropertyChanged;
resultCollection.Add(newPoint);
}
resultCollection.CollectionChanged += OnSomeObservableCollectionCollectionChanged;
return resultCollection;
}
private void OnSomeObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (TClass TClass in e.NewItems)
{
TClass.PropertyChanged += OnItemPropertyChanged;
}
}
if (e.OldItems != null)
{
foreach (TClass TClass in e.OldItems)
{
TClass.PropertyChanged -= OnItemPropertyChanged;
}
}
if (!Patient.HasChanges)
Patient.HasChanges = true;
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "ItemArchivedProperty") return;
// set Archived = true or set Archived = false
}
This is just an example, but it should works. Hope it helps.

Binding a property that autoupdates

I wasn't expecting this to work but I wanted to try it. I have a bool that should tell me if the user has selected anything in my application (inside a particular control).
I wanted to bind something to that bool:
private bool _isAnythingSelected;
public bool IsAnythingSelected
{
get
{
_isAnythingSelected = (MyModel.Series.Where(p => p.IsSelected && p.GetType() == typeof(LineSeries)).Count() > 0);
return _isAnythingSelected;
}
set
{
_isAnythingSelected = value;
RaisePropertyChanged("IsAnythingSelected");
}
}
This is not working as I would like it, and I understand why it is so. My question is, how should I implement this selection thing without going to every method that allows user to select things? Thank you.
To update IsAnythingSelected properly, you have to handle two kinds of notifications:
notification about MyModel.Series collection changed;
notification about MyModel.Series collection item property changed.
The first one can be achieved "out of the box" with ObservableCollection<T> (or any other collection, that implements INotifyCollectionChanged.
The second one requires a custom solution (at least, I don't know any existing "out of the box" one).
You can combine ObservableCollection<T> with item property changed handling this way:
class MyObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
private void Initialize()
{
// initial PropertyChanged subscription
foreach (var item in Items)
{
SubscribeItemPropertyChanged(item);
}
}
private void SubscribeItemPropertyChanged(object item)
{
((INotifyPropertyChanged)item).PropertyChanged += HandleItemPropertyChanged;
}
private void UnSubscribeItemPropertyChanged(object item)
{
((INotifyPropertyChanged)item).PropertyChanged -= HandleItemPropertyChanged;
}
protected virtual void HandleItemPropertyChanged(object sender, PropertyChangedEventArgs args)
{
var handler = ItemPropertyChanged;
if (handler != null)
{
handler(sender, args);
}
}
protected override void ClearItems()
{
// we should unsubscribe from INotifyPropertyChanged.PropertyChanged event for each item
Items.ToList().ForEach(item => Remove(item));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
// subscribe for new items property changing;
// un-subscribe for old items property changing
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SubscribeItemPropertyChanged(e.NewItems[0]);
break;
case NotifyCollectionChangedAction.Remove:
UnSubscribeItemPropertyChanged(e.OldItems[0]);
break;
case NotifyCollectionChangedAction.Replace:
SubscribeItemPropertyChanged(e.NewItems[0]);
UnSubscribeItemPropertyChanged(e.OldItems[0]);
break;
}
}
public MyObservableCollection()
: base()
{
}
public MyObservableCollection(IEnumerable<T> collection)
: base(collection)
{
Initialize();
}
public MyObservableCollection(List<T> list)
: base(list)
{
Initialize();
}
public EventHandler<PropertyChangedEventArgs> ItemPropertyChanged;
}
...and make MyModel.Series a MyObservableCollection<T> instance.
Then, your class, which contains IsAnythingSelected, will look like this:
// somewhere in code, where `MyModel` being initialized:
MyModel.Series.CollectionChanged += (sender, args) => RaisePropertyChanged("IsAnythingSelected");
MyModel.Series.ItemPropertyChanged += (sender, args) => RaisePropertyChanged("IsAnythingSelected");
public bool IsAnythingSelected
{
get
{
return MyModel.Series.Any(p => p.IsSelected && p.GetType() == typeof(LineSeries));
}
}
You even don't need IsAnythingSelected to be read-write property in this case. Just notify, that it was changed, and binding engine will re-read its value.

Datagrid does not maintain sort when the items are updated

I have a nice WPF DataGrid binded to a collection of objects. Everything works fine, when the properties of any object change the grid is updated. The problem is that the rows are not re-sorted when the update happens and then the sort is not valid anymore.
Any idea on how to fix that?
Thanks in advance.
EDIT: This is how I'm binding the DataGrid:
<Controls:DataGrid MinHeight="300" MinWidth="300" ItemsSource="{Binding Data}" AutoGenerateColumns="True">
public class MainWindowViewModel
{
public ObservableCollectionMultiThread<StockViewModel> Data { get; private set; }
}
public class ObservableCollectionMultiThread<T> : ObservableCollection<T>
{
// Override the event so this class can access it
public override event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// Be nice - use BlockReentrancy like MSDN said
using (base.BlockReentrancy())
{
System.Collections.Specialized.NotifyCollectionChangedEventHandler eventHandler = this.CollectionChanged;
if (eventHandler == null)
return;
Delegate[] delegates = eventHandler.GetInvocationList();
// Walk thru invocation list
foreach (System.Collections.Specialized.NotifyCollectionChangedEventHandler handler in delegates)
{
DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
// If the subscriber is a DispatcherObject and different thread
if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
{
// Invoke handler in the target dispatcher's thread
dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
}
else // Execute handler as is
handler(this, e);
}
}
}
}
PD: I'm using the DataGrid from CTP October 2008 as I'm with Net 3.5
Usually controls on WPF does not respond to item edition. I've used this class as the source for the grid:
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Threading;
namespace StockCrawler
{
public class AutoRefreshListCollectionView : ListCollectionView
{
public AutoRefreshListCollectionView(IList list)
: base(list)
{
this.SubscribeSourceEvents(list, false);
}
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
bool refresh = false;
foreach (SortDescription sort in this.SortDescriptions)
{
if (sort.PropertyName == e.PropertyName)
{
refresh = true;
break;
}
}
if (!refresh)
{
foreach (GroupDescription group in this.GroupDescriptions)
{
PropertyGroupDescription propertyGroup = group as PropertyGroupDescription;
if (propertyGroup != null && propertyGroup.PropertyName == e.PropertyName)
{
refresh = true;
break;
}
}
}
if (refresh)
{
if (!this.Dispatcher.CheckAccess())
{
// Invoke handler in the target dispatcher's thread
this.Dispatcher.Invoke((Action)this.Refresh);
}
else // Execute handler as is
{
this.Refresh();
}
}
}
private void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
this.SubscribeItemsEvents(e.NewItems, false);
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
this.SubscribeItemsEvents(e.OldItems, true);
}
else
{
// TODO: Support this
}
}
private void SubscribeItemEvents(object item, bool remove)
{
INotifyPropertyChanged notify = item as INotifyPropertyChanged;
if (notify != null)
{
if (remove)
{
notify.PropertyChanged -= this.Item_PropertyChanged;
}
else
{
notify.PropertyChanged += this.Item_PropertyChanged;
}
}
}
private void SubscribeItemsEvents(IEnumerable items, bool remove)
{
foreach (object item in items)
{
this.SubscribeItemEvents(item, remove);
}
}
private void SubscribeSourceEvents(object source, bool remove)
{
INotifyCollectionChanged notify = source as INotifyCollectionChanged;
if (notify != null)
{
if (remove)
{
notify.CollectionChanged -= this.Source_CollectionChanged;
}
else
{
notify.CollectionChanged += this.Source_CollectionChanged;
}
}
this.SubscribeItemsEvents((IEnumerable)source, remove);
}
}
}

Categories

Resources