I've seen lots of talk about this question but maybe I'm just too much of a newbie to get it. If I have an observable collection that is a collection of "PersonNames" as in the msdn example (http: //msdn.microsoft.com/en-us/library/ms748365.aspx), I get updates to my View if a PersonName is added or removed, etc. I want to get an update to my View when I change a property in the PersonName as well. Like if I change the first name. I can implement OnPropertyChanged for each property and have this class derive from INotifyPropertyChanged and that seems to get called as expected.
My question is, how does the View get the updated data from the ObservableCollection as the property changed does not cause any event for the ObservableCollection?
This is probably something really simple but why I can't seem to find an example surprises me. Can anyone shed any light on this for me or have any pointers to examples I would greatly appreciate it. We have this scenario in multiple places in our current WPF app and are struggling with figuring it out.
"Generally, the code responsible for displaying the data adds a PropertyChanged event handler to each object currently displayed onscreen."
Could someone please give me an example of what this means? My View binds to my ViewModel which has a ObservableCollection. This collection is made up of a RowViewModel which has properties that support the PropertiesChanged event. But I can't figure out how to make the collection update itself so my view will be updated.
Here is how you would attach/detach to each item's PropertyChanged event.
ObservableCollection<INotifyPropertyChanged> items = new ObservableCollection<INotifyPropertyChanged>();
items.CollectionChanged += items_CollectionChanged;
static void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= item_PropertyChanged;
}
if (e.NewItems != null)
{
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += item_PropertyChanged;
}
}
static void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
We wrote this in the WPF-chat:
public class OcPropertyChangedListener<T> : INotifyPropertyChanged where T : INotifyPropertyChanged
{
private readonly ObservableCollection<T> _collection;
private readonly string _propertyName;
private readonly Dictionary<T, int> _items = new Dictionary<T, int>(new ObjectIdentityComparer());
public OcPropertyChangedListener(ObservableCollection<T> collection, string propertyName = "")
{
_collection = collection;
_propertyName = propertyName ?? "";
AddRange(collection);
CollectionChangedEventManager.AddHandler(collection, CollectionChanged);
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddRange(e.NewItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Remove:
RemoveRange(e.OldItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Replace:
AddRange(e.NewItems.Cast<T>());
RemoveRange(e.OldItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Reset:
Reset();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void AddRange(IEnumerable<T> newItems)
{
foreach (T item in newItems)
{
if (_items.ContainsKey(item))
{
_items[item]++;
}
else
{
_items.Add(item, 1);
PropertyChangedEventManager.AddHandler(item, ChildPropertyChanged, _propertyName);
}
}
}
private void RemoveRange(IEnumerable<T> oldItems)
{
foreach (T item in oldItems)
{
_items[item]--;
if (_items[item] == 0)
{
_items.Remove(item);
PropertyChangedEventManager.RemoveHandler(item, ChildPropertyChanged, _propertyName);
}
}
}
private void Reset()
{
foreach (T item in _items.Keys.ToList())
{
PropertyChangedEventManager.RemoveHandler(item, ChildPropertyChanged, _propertyName);
_items.Remove(item);
}
AddRange(_collection);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void ChildPropertyChanged(object sender, PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(sender, e);
}
private class ObjectIdentityComparer : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return object.ReferenceEquals(x, y);
}
public int GetHashCode(T obj)
{
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}
}
public static class OcPropertyChangedListener
{
public static OcPropertyChangedListener<T> Create<T>(ObservableCollection<T> collection, string propertyName = "") where T : INotifyPropertyChanged
{
return new OcPropertyChangedListener<T>(collection, propertyName);
}
}
Weak events
Keeps track of the same item being added multiple times to the collection
It ~bubbles~ up the property changed events of the children.
The static class is just for convenience.
Use it like this:
var listener = OcPropertyChangedListener.Create(yourCollection);
listener.PropertyChanged += (sender, args) => { //do you stuff}
Bill,
I'm sure that you have found a workaround or solution to your issue by now, but I posted this for anyone with this common issue. You can substitute this class for ObservableCollections that are collections of objects that implement INotifyPropertyChanged. It is kind of draconian, because it says that the list needs to Reset rather than find the one property/item that has changed, but for small lists the performance hit should be unoticable.
Marc
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace WCIOPublishing.Helpers
{
public class ObservableCollectionWithItemNotify<T> : ObservableCollection<T> where T: INotifyPropertyChanged
{
public ObservableCollectionWithItemNotify()
{
this.CollectionChanged += items_CollectionChanged;
}
public ObservableCollectionWithItemNotify(IEnumerable<T> collection) :base( collection)
{
this.CollectionChanged += items_CollectionChanged;
foreach (INotifyPropertyChanged item in collection)
item.PropertyChanged += item_PropertyChanged;
}
private void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e != null)
{
if(e.OldItems!=null)
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= item_PropertyChanged;
if(e.NewItems!=null)
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += item_PropertyChanged;
}
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
this.OnCollectionChanged(reset);
}
}
}
As you found out, there is no collection-level event that indicates that a property of an item in the collection has changed. Generally, the code responsible for displaying the data adds a PropertyChanged event handler to each object currently displayed onscreen.
Instead of ObservableCollection simply use the BindingList<T>.
The following code shows a DataGrid binding to a List and to item's properties.
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn Header="Values" Binding="{Binding Value}" />
</DataGrid.Columns>
</DataGrid>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Threading;
namespace WpfApplication1 {
public partial class MainWindow : Window {
public MainWindow() {
var c = new BindingList<Data>();
this.DataContext = c;
// add new item to list on each timer tick
var t = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
t.Tick += (s, e) => {
if (c.Count >= 10) t.Stop();
c.Add(new Data());
};
t.Start();
}
}
public class Data : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged = delegate { };
System.Timers.Timer t;
static Random r = new Random();
public Data() {
// update value on each timer tick
t = new System.Timers.Timer() { Interval = r.Next(500, 1000) };
t.Elapsed += (s, e) => {
Value = DateTime.Now.Ticks;
this.PropertyChanged(this, new PropertyChangedEventArgs("Value"));
};
t.Start();
}
public long Value { get; private set; }
}
}
Following is the code giving a simple explanation of answer by #Stack and showing how BindingList is observing if it has a item changed and shows ObservableCollection will not observe the change inside an item.
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace BindingListExample
{
class Program
{
public ObservableCollection<MyStruct> oc = new ObservableCollection<MyStruct>();
public System.ComponentModel.BindingList<MyStruct> bl = new BindingList<MyStruct>();
public Program()
{
oc.Add(new MyStruct());
oc.CollectionChanged += CollectionChanged;
bl.Add(new MyStruct());
bl.ListChanged += ListChanged;
}
void ListChanged(object sender, ListChangedEventArgs e)
{
//Observe when the IsActive value is changed this event is triggered.
Console.WriteLine(e.ListChangedType.ToString());
}
void CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Observe when the IsActive value is changed this event is not triggered.
Console.WriteLine(e.Action.ToString());
}
static void Main(string[] args)
{
Program pm = new Program();
pm.bl[0].IsActive = false;
}
}
public class MyStruct : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isactive;
public bool IsActive
{
get { return isactive; }
set
{
isactive = value;
NotifyPropertyChanged("IsActive");
}
}
private void NotifyPropertyChanged(String PropertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
}
}
Related
I'm new to C# and I'm trying to get a change in my datagrid to trigger a method in my main.
I've got my class:
public class siteMatch : INotifyPropertyChanged
{
public string SelectedTag
{
get { return _SelectedTag; }
set
{
if (value != _SelectedTag)
{
_SelectedTag = value;
OnPropertyChanged(_SelectedTag);
}
}
}
private string _SelectedTag;
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, e);
}
protected void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
And I've got this in my main class:
public partial class MainWindow : Window
{
public ObservableCollection<siteMatch> sitesMatched = new ObservableCollection<siteMatch>();
public MainWindow()
{
InitializeComponent();
sitesMatched.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(items_CollectionChanged);
}
static void items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
static void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//throw new NotImplementedException();
}
}
Am I missing something here? The items_CollectionChanged method doesn't get triggered when I change something on the datagrid. I have a feeling that I haven't subscribed to the event properly. The OnPropertyChanged method gets triggered correctly but nothing happens after that.
Any help will be greatly appreciated.
Thanks,
Edit: I should point out that I only have access to .Net 4
It is my understanding that the Collection Changed is only triggered on creation of the collection, or in addition or deletion, but not when you change something inside the collection.
What you want to accomplish can be done with the events on the Datagrid itself,
e.g. by using the DataGridView.CellValueChanged Event.
Another solution is for you to expand the ObservableCollection class.
Have a look here : https://stackoverflow.com/a/5256827/3042778
I am having trouble with getting a total count calculation to run when the value of one of the items in a collection is changed. I do have the INotifyPropertChanged event on the class and the collection is an ObservableCollection<T>. I have also tried a BindingList<T> without luck.
public class Inventory
{
public ObservableCollection<Item> Items { get; set; }
public int TotalCount {
get { return Items.Select(i => i.Count).Sum(); }
}
}
public class Item : INotifyPropertyChanged {
private int count;
public int Count {
get { return count; }
set {
count = value;
NotifyPropertyChanged("Count");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName) {
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
My next though was to register an event handler for each item that is added to the collection, but I am not quite sure how to manage the events that may have already been registered. It seems I am missing something simple here, can anyone point it out?
BTW, I am data binding in WPF to this class.
You need to handle CollectionChanged event, subscribe to newly added items and unsubscribe from deleted items. This is a tedious task to do it every time, so you can subclass ObservableCollection and override OnCollectionChanged.
For an example implementation, see GitHub/Alba.Framework/NotifyingCollection(T).cs (it has some dependencies within the project, so you won't be able to copy-paste it though). There's a simpler implementation on StackOverflow somewhere, but I can't find it at the moment.
Here is the code for that "tedious task" ... first in the Inventory constructor:
Items.CollectionChanged += Items_CollectionChanged;
Then in the body of the Inventory class:
void Items_CollectionChanged(object s, NotifyCollectionChangedEventArgs e) {
if (e.OldItems != null)
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= ItemPropertyChanged;
if (e.NewItems != null)
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += ItemPropertyChanged;
}
void ItemPropertyChanged(object s, PropertyChangedEventArgs e) {
if (e.PropertyName == "Count")
NotifyPropertyChanged("TotalCount");
}
I found on this link
ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)
some techniques to notify a Observablecollection that an item has changed. the TrulyObservableCollection in this link seems to be what i'm looking for.
public class TrulyObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
public TrulyObservableCollection()
: base()
{
CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
}
void TrulyObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Object item in e.NewItems)
{
(item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
if (e.OldItems != null)
{
foreach (Object item in e.OldItems)
{
(item as INotifyPropertyChanged).PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyCollectionChangedEventArgs a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
OnCollectionChanged(a);
}
}
But when I try to use it, I don't get notifications on the collection. I'm not sure how to correctly implement this in my C# Code:
XAML :
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MyItemsSource, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding MyProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataGrid.Columns>
</DataGrid>
ViewModel :
public class MyViewModel : ViewModelBase
{
private TrulyObservableCollection<MyType> myItemsSource;
public TrulyObservableCollection<MyType> MyItemsSource
{
get { return myItemsSource; }
set
{
myItemsSource = value;
// Code to trig on item change...
RaisePropertyChangedEvent("MyItemsSource");
}
}
public MyViewModel()
{
MyItemsSource = new TrulyObservableCollection<MyType>()
{
new MyType() { MyProperty = false },
new MyType() { MyProperty = true },
new MyType() { MyProperty = false }
};
}
}
public class MyType : ViewModelBase
{
private bool myProperty;
public bool MyProperty
{
get { return myProperty; }
set
{
myProperty = value;
RaisePropertyChangedEvent("MyProperty");
}
}
}
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChangedEvent(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, e);
}
}
}
When i run the program, i have the 3 checkbox to false, true, false as in the property initialisation.
but when i change the state of one of the ckeckbox, the program go through item_PropertyChanged but never in MyItemsSource Property code.
The spot you have commented as // Code to trig on item change... will only trigger when the collection object gets changed, such as when it gets set to a new object, or set to null.
With your current implementation of TrulyObservableCollection, to handle the property changed events of your collection, register something to the CollectionChanged event of MyItemsSource
public MyViewModel()
{
MyItemsSource = new TrulyObservableCollection<MyType>();
MyItemsSource.CollectionChanged += MyItemsSource_CollectionChanged;
MyItemsSource.Add(new MyType() { MyProperty = false });
MyItemsSource.Add(new MyType() { MyProperty = true});
MyItemsSource.Add(new MyType() { MyProperty = false });
}
void MyItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Handle here
}
Personally I really don't like this implementation. You are raising a CollectionChanged event that says the entire collection has been reset, anytime a property changes. Sure it'll make the UI update anytime an item in the collection changes, but I see that being bad on performance, and it doesn't seem to have a way to identify what property changed, which is one of the key pieces of information I usually need when doing something on PropertyChanged.
I prefer using a regular ObservableCollection and just hooking up the PropertyChanged events to it's items on CollectionChanged. Providing your UI is bound correctly to the items in the ObservableCollection, you shouldn't need to tell the UI to update when a property on an item in the collection changes.
public MyViewModel()
{
MyItemsSource = new ObservableCollection<MyType>();
MyItemsSource.CollectionChanged += MyItemsSource_CollectionChanged;
MyItemsSource.Add(new MyType() { MyProperty = false });
MyItemsSource.Add(new MyType() { MyProperty = true});
MyItemsSource.Add(new MyType() { MyProperty = false });
}
void MyItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach(MyType item in e.NewItems)
item.PropertyChanged += MyType_PropertyChanged;
if (e.OldItems != null)
foreach(MyType item in e.OldItems)
item.PropertyChanged -= MyType_PropertyChanged;
}
void MyType_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "MyProperty")
DoWork();
}
A simple solution is to use BindingList<T> instead of ObservableCollection<T> . Indeed the BindingList relay item change notifications. So with a binding list, if the item implements the interface INotifyPropertyChanged then you can simply get notifications using the ListChanged event.
See also this SO answer.
I solved this case by using static Action
public class CatalogoModel
{
private String _Id;
private String _Descripcion;
private Boolean _IsChecked;
public String Id
{
get { return _Id; }
set { _Id = value; }
}
public String Descripcion
{
get { return _Descripcion; }
set { _Descripcion = value; }
}
public Boolean IsChecked
{
get { return _IsChecked; }
set
{
_IsChecked = value;
NotifyPropertyChanged("IsChecked");
OnItemChecked.Invoke();
}
}
public static Action OnItemChecked;
}
public class ReglaViewModel : ViewModelBase
{
private ObservableCollection<CatalogoModel> _origenes;
CatalogoModel.OnItemChecked = () =>
{
var x = Origenes.Count; //Entra cada vez que cambia algo en _origenes
};
}
You could use an extension method to get notified about changed property of an item in a collection in a generic way.
public static class ObservableCollectionExtension
{
public static void NotifyPropertyChanged<T>(this ObservableCollection<T> observableCollection, Action<T, PropertyChangedEventArgs> callBackAction)
where T : INotifyPropertyChanged
{
observableCollection.CollectionChanged += (sender, args) =>
{
//Does not prevent garbage collection says: http://stackoverflow.com/questions/298261/do-event-handlers-stop-garbage-collection-from-occuring
//publisher.SomeEvent += target.SomeHandler;
//then "publisher" will keep "target" alive, but "target" will not keep "publisher" alive.
if (args.NewItems == null) return;
foreach (T item in args.NewItems)
{
item.PropertyChanged += (obj, eventArgs) =>
{
callBackAction((T)obj, eventArgs);
};
}
};
}
}
public void ExampleUsage()
{
var myObservableCollection = new ObservableCollection<MyTypeWithNotifyPropertyChanged>();
myObservableCollection.NotifyPropertyChanged((obj, notifyPropertyChangedEventArgs) =>
{
//DO here what you want when a property of an item in the collection has changed.
});
}
I know it's late, but maybe this helps others. I have created a class NotifyObservableCollection, that solves the problem of missing notification to item itself, when a property of the item changes. The usage is as simple as ObservableCollection.
public class NotifyObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
private void Handle(object sender, PropertyChangedEventArgs args)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, null));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null) {
foreach (object t in e.NewItems) {
((T) t).PropertyChanged += Handle;
}
}
if (e.OldItems != null) {
foreach (object t in e.OldItems) {
((T) t).PropertyChanged -= Handle;
}
}
base.OnCollectionChanged(e);
}
While Items are added or removed the class forwards the items PropertyChanged event to the collections PropertyChanged event.
usage:
public abstract class ParameterBase : INotifyPropertyChanged
{
protected readonly CultureInfo Ci = new CultureInfo("en-US");
private string _value;
public string Value {
get { return _value; }
set {
if (value == _value) return;
_value = value;
OnPropertyChanged();
}
}
}
public class AItem {
public NotifyObservableCollection<ParameterBase> Parameters {
get { return _parameters; }
set {
NotifyCollectionChangedEventHandler cceh = (sender, args) => OnPropertyChanged();
if (_parameters != null) _parameters.CollectionChanged -= cceh;
_parameters = value;
//needed for Binding to AItem at xaml directly
_parameters.CollectionChanged += cceh;
}
}
public NotifyObservableCollection<ParameterBase> DefaultParameters {
get { return _defaultParameters; }
set {
NotifyCollectionChangedEventHandler cceh = (sender, args) => OnPropertyChanged();
if (_defaultParameters != null) _defaultParameters.CollectionChanged -= cceh;
_defaultParameters = value;
//needed for Binding to AItem at xaml directly
_defaultParameters.CollectionChanged += cceh;
}
}
public class MyViewModel {
public NotifyObservableCollection<AItem> DataItems { get; set; }
}
If now a property of an item in DataItems changes, the following xaml will get a notification, though it binds to Parameters[0] or to the item itself except to the changing property Value of the item (Converters at Triggers are called reliable on every change).
<DataGrid CanUserAddRows="False" AutoGenerateColumns="False" ItemsSource="{Binding DataItems}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Parameters[0].Value}" Header="P1">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background" Value="Aqua" />
<Style.Triggers>
<DataTrigger Value="False">
<!-- Bind to Items with changing properties -->
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource ParameterCompareConverter}">
<Binding Path="DefaultParameters[0]" />
<Binding Path="Parameters[0]" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="DeepPink" />
</DataTrigger>
<!-- Binds to AItem directly -->
<DataTrigger Value="True" Binding="{Binding Converter={StaticResource CheckParametersConverter}}">
<Setter Property="FontWeight" Value="ExtraBold" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
All the solutions here are correct,but they are missing an important scenario in which the method Clear() is used, which doesn't provide OldItems in the NotifyCollectionChangedEventArgs object.
this is the perfect ObservableCollection .
public delegate void ListedItemPropertyChangedEventHandler(IList SourceList, object Item, PropertyChangedEventArgs e);
public class ObservableCollectionEX<T> : ObservableCollection<T>
{
#region Constructors
public ObservableCollectionEX() : base()
{
CollectionChanged += ObservableCollection_CollectionChanged;
}
public ObservableCollectionEX(IEnumerable<T> c) : base(c)
{
CollectionChanged += ObservableCollection_CollectionChanged;
}
public ObservableCollectionEX(List<T> l) : base(l)
{
CollectionChanged += ObservableCollection_CollectionChanged;
}
#endregion
public new void Clear()
{
foreach (var item in this)
if (item is INotifyPropertyChanged i)
i.PropertyChanged -= Element_PropertyChanged;
base.Clear();
}
private void ObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
foreach (var item in e.OldItems)
if (item != null && item is INotifyPropertyChanged i)
i.PropertyChanged -= Element_PropertyChanged;
if (e.NewItems != null)
foreach (var item in e.NewItems)
if (item != null && item is INotifyPropertyChanged i)
{
i.PropertyChanged -= Element_PropertyChanged;
i.PropertyChanged += Element_PropertyChanged;
}
}
}
private void Element_PropertyChanged(object sender, PropertyChangedEventArgs e) => ItemPropertyChanged?.Invoke(this, sender, e);
public ListedItemPropertyChangedEventHandler ItemPropertyChanged;
}
The ObservableCollection and its derivatives raises its property changes internally. The code in your setter should only be triggered if you assign a new TrulyObservableCollection<MyType> to the MyItemsSource property. That is, it should only happen once, from the constructor.
From that point forward, you'll get property change notifications from the collection, not from the setter in your viewmodel.
One simple solution to this is to replace the item being changed in the ObservableCollection which notifies the collection of the changed item. In the sample code snippet below Artists is the ObservableCollection and artist is an item of the type in the ObservableCollection:
var index = Artists.IndexOf(artist);
Artists.RemoveAt(index);
artist.IsFollowed = true; // change something in the item
Artists.Insert(index, artist);
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);
}
}
}
I have a WPF dialog that is bound to a list of ObservableCollection<MyEntity> type. In the dialog, I want the "OK" button to be enabled only if changes are made to the ObservableCollection<MyEntity> list - that includes adding/removing items from the list and modifying the individual items in the list.
For adding/removing items from the list, it is easy - I implemented a handler for the CollectionChanged event.
What I don't know how to do is when an individual item is modified. Say, MyEntity.Name="New Value", what interface does MyEntity class need to implement to make it 'observable'?
MyEntity needs to implement INotifyPropertyChanged, then when a property change occurs you fire the PropertyChanged event. Like this:
public class MyEntity : INotifyPropertyChanged
{
public bool MyFlag
{
get { return _myFlag; }
set
{
_myFlag = value;
OnPropertyChanged("MyFlag");
}
}
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Two ways to approach this are:
have an event listener internal to the object which then sets an IsDirty flag whenever a property changes. Then OK button is bound to a command (check out the usage of the ICommand interface), and in the CanExecute method of the command you check if any of the objects in the ObservableCollection have been set to dirty. This check can be done with a simple LINQ statement: myCollection.Any(x => x.IsDirty == true)
this method is more clunky and smelly.... have an external object listening for changes (by subscribing to the PropertyChanged event on each object), and that external listener can then enable the OK button (via databinding or by setting it directly).
I like the answer provided by slugster, here is an alternative building on slugster's answer.
If you bind to your OK button using DelegateCommnd you can add event handlers for CollectionChanged and PropertyChanged to change a simple boolean flag to control the state of the OK button.
public class MainViewModel : ViewModelBase
{
public DelegateCommand<object> RunCommand { get; set; }
public DelegateCommand<object> OkCommand { get; set; }
private bool enableOk = false;
private bool setOK = false;
private ObservableCollection<MyEntity> _entites = new ObservableCollection<MyEntity>();
public MainViewModel()
{
_entites.CollectionChanged += (s, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// handle property changing
foreach (MyEntity item in e.NewItems)
{
((INotifyPropertyChanged)item).PropertyChanged += (s1, e1) => { if (setOK) enableOk = true; };
}
}
// handle collection changing
if (setOK) enableOk = false;
};
MyEntity me1 = new MyEntity { Name = "Name", Information = "Information", Details = "Detials" };
MyEntity me2 = new MyEntity { Name = "Name", Information = "Information", Details = "Detials" };
MyEntity me3 = new MyEntity { Name = "Name", Information = "Information", Details = "Detials" };
_entites.Add(me1);
_entites.Add(me2);
_entites.Add(me3);
// allow collection changes now to start enabling the ok button...
setOK = true;
RunCommand = new DelegateCommand<object>(OnRunCommnad, CanRunCommand);
OkCommand = new DelegateCommand<object>(OnOkCommnad, CanOkCommand);
}
private void OnRunCommnad(object obj)
{
MyEntity me = new MyEntity { Name = "Name", Information = "Information", Details = "Detials" };
// causes ok to become enabled
_entites.Add(me);
MyEntity first = _entites[0];
// causes ok to become enabled
first.Name = "Zamboni";
}
private bool CanRunCommand(object obj)
{
return true;
}
private void OnOkCommnad(object obj)
{
}
private bool CanOkCommand(object obj)
{
return enableOk;
}
}
Here is a version MyEntity (similar to the one provided by slugster):
Only the Name property fires an event in this example...
public class MyEntity : INotifyPropertyChanged
{
private string _name = string.Empty;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged("Name");
}
}
public string Information { get; set; }
public string Details { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You should implement INotifyPropertyChanged. You could do it by the following way
(as you can see, this implementation is fully thread safe)
private readonly object _sync = new object();
public event PropertyChangedEventHandler PropertyChanged
{
add { lock (_sync) _propertyChanged += value; }
remove { lock (_sync) _propertyChanged -= value; }
} private PropertyChangedEventHandler _propertyChanged;
protected void OnPropertyChanged(Expression<Func<object>> propertyExpression)
{
OnPropertyChanged(GetPropertyName(propertyExpression));
}
protected string GetPropertyName(Expression<Func<object>> propertyExpression)
{
MemberExpression body;
if (propertyExpression.Body is UnaryExpression)
body = (MemberExpression) ((UnaryExpression) propertyExpression.Body).Operand;
else
body = (MemberExpression) propertyExpression.Body;
return body.Member.Name;
}
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = _propertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
Following the implementation I described above, you can notify about your changes by two ways
1) The first way
public int MyProperty
{
get { return _myProperty; }
set
{
if (value != __myProperty)
{
_subVersion = value;
OnPropertyChanged(MyPropertyPropertyName);
}
}
} private int _myProperty; const string MyPropertyPropertyName = "MyProperty";
2) And the second way
public int MyProperty
{
get { return _myProperty; }
set
{
if (value != _myProperty)
{
_subVersion = value;
OnPropertyChanged(() => MyProperty);
}
}
} private int _myProperty;
Another solution could be a custom observable collection that requires items to implement INotifyPropertyChanged. The user must attach a handler to the OnItemPropertyChanged event, which will be called whenever the property of an item in the collection is changed.
public class ObservableCollectionEnhanced<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public ObservableCollectionEnhanced()
: base()
{ }
public ObservableCollectionEnhanced(IEnumerable<T> collection)
: base(collection)
{
foreach (T item in Items)
item.PropertyChanged += OnItemPropertyChanged;
}
public ObservableCollectionEnhanced(List<T> list)
: base(list)
{
foreach (T item in Items)
item.PropertyChanged += OnItemPropertyChanged;
}
public event System.ComponentModel.PropertyChangedEventHandler ItemPropertyChanged;
public void OnItemPropertyChanged(Object sender, PropertyChangedEventArgs e)
{
if (null != ItemPropertyChanged)
ItemPropertyChanged(sender, e);
}
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
item.PropertyChanged += OnItemPropertyChanged;
}
protected override void RemoveItem(int index)
{
T item = this.Items[index];
item.PropertyChanged -= OnItemPropertyChanged;
base.RemoveItem(index);
}
protected override void SetItem(int index, T item)
{
T oldItem = Items[index];
base.SetItem(index, item);
oldItem.PropertyChanged -= OnItemPropertyChanged;
item.PropertyChanged += OnItemPropertyChanged;
}
}
Configure the handler as follows:
public void OnItemPropertyChanged(Object sender, PropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("Update called on {0}", sender);
}
...
collection.ItemPropertyChanged += OnItemPropertyChanged;