Notify ObservableCollection when Item changes
Asked Answered
D

8

55

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.

Demagoguery answered 13/12, 2011 at 14:6 Comment(5)
Have you tried tracing the RaisePropertyChangedEvent method in the debugger? In other words, does the control goes into the if block?Schreiber
ObservableCollection isn't supposed to raise CollectionChanged when a property changes on one of the items in the collection. Because the collection didn't change. I can't figure out what you're actually trying to do, but I think you might benefit from looking at ContinuousLINQ -- clinq.codeplex.com.Gavin
I used the code. The problem is that you unsubscribe from all the old items' property changed event. I commented that part out and everything worked smoothly.Mcgraw
Instead of TrulyObservableCollection just use System.ComponentModel.BindingList<T>Sipple
Does this answer your question? A better way of forcing data bound WPF ListBox to update?Storms
W
98

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();
}
Whacking answered 13/12, 2011 at 14:39 Comment(15)
thanks ! you are right about the perf because the dataGrid was twinkling on checkbox click. Just one thing to make the first added items subscribe to Propertychange in the constructor, MyItemsSource must be initialized after subscribing CollectionChange eventDemagoguery
This doesn't work if you have the XAML binding directly to the item itself, instead of a property on the item. In my case (if you refer to his example), change a line in the XAML to look something like this: <DataGridCheckBoxColumn Binding="{Binding}"/> Anyone have a solution for this situation?Volume
@Rachel, thx for the answer. I need a bit more clarification on this problem though. I have observable collection of T, where T implements INotifyPropertyChanged. When I update a property of T, I can see that the MyType_PropertyChanged get called but my View ( An ItemControl, the ItemSource is bound to the ObservableCollection) DOES NOT update. I made the viewmodel to implement the INotifyPropertyChanged too, but it doesnot help either.Protoplast
@FrankLiu An ObservableCollection.CollectionChanged only triggers when the collection itself changes - either changes to a new collection, or a new item gets added, or an item gets deleted. It does not trigger when an item inside the collection triggers a PropertyChange notification. The code here hooks up a PropertyChanged event handler to each item to trigger the CollectionChanged whenever it's PropertyChanged gets firedWhacking
@Rachel, How do I raise the CollectionChanged event of the ObservableCollection in the MyType_PropertyChanged event handler? Could you please be a little bit more specific? Thank you.Protoplast
@FrankLiu There's an example here if you're interestedWhacking
@Whacking Again, thx for your reply. I raised the PropertyChanged event for my ObservableCollection by calling OnPropertyChanged("MyCollection") in the MyType_PropertyChanged event handler. The event does got raised when a property of an item inside the collection is updated. But the ItemsControl whose ItemSource is bound to MyCollection is still not updated/refreshed. It seems to me that raise PropertyChanged event of the ObservableCollection does not raise the ObservableCollection.CollectionChanged event, which the ItemsControl is listening to.Protoplast
@Whacking "I really don't like this implementation..." - I've put up an answer to the question from which TrulyObservableCollection<> originated with a slightly more sophisticated solution to the problem, FullyObservableCollection<>. I think it solves some of your problems with this version.Mcduffie
what happens if delete the observable collection (e.g., Items = new ObservableCollection<..>), will the PropertyChanged get cleared out?Mediant
@Mediant The CollectionChanged handler was attached to the old instance of ObservableCollection, so would not apply to the new instance. Ideally, you should remember to detach the event handler in the case you ever dispose of the old collection.Whacking
this solution is good enough to handle many cases, but there is a little problem when the items of the observable collection are observable collections. in this case the CollectionChanged event wont' be raised when an item gets added or deleted from the second-level ObservableCollection. in other words, the main ObservableCollection will be fully blinded to any changes happens on second-level collection.Libna
also another case where the item model contains a property (or properties) of ObservableCollectionLibna
Hi! How did you define your MyItemSource property? Is it public ObservableCollection<FTTransactionList> FTTransactions { get { return _FTTransactions; } set { _FTTransactions = value; } }?Broucek
@FrankLiu Did you found a solution for the problem you mentioned in your comment above from Feb 9 '15? I have exactly the same problem and calling "OnPropertyChanged(nameof(TheObservableCollection))" in the item-changed-handler is not sufficient to update a target dependency property in another class that is bound to "TheObservableCollection".Mckenna
Sometimes the collection is Reset and as this code hints, OldItems can be null, isnt this a memory leak as the event hooks stay but the source is gone?Herrah
T
14

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.

Torray answered 19/6, 2018 at 13:56 Comment(5)
Just what I was looking for. Couldn't remember the name of the type.Unbutton
I spend hours of trying the above examples and failing. Then I found this answer, and it instantly worked! This should have much more votes, because it provides a fast and simple solution! In the linked answers you will find some disadvantages, but in my case it solved the problem.Hairworm
For those who arrived here looking in the WinUI3 context, it appears BindingList is not compatible, or at least is not a drop-in replacement for ObservableCollection.Euroclydon
This is not a simple solution, this is the RIGHT solution, works for MAUI.Buskin
Another problem with BindingList<T> is that its default CollectionView of type BindingListCollectionView does not support Filtering, which may limit its use: https://mcmap.net/q/130850/-39-system-windows-data-bindinglistcollectionview-39-view-does-not-support-filteringMedlin
O
10

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
            };
}
Oily answered 1/8, 2013 at 23:24 Comment(1)
That's a good one for scenarios where the only instances of the item to check are in that ObservableCollection.Constringent
B
4

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: https://mcmap.net/q/126379/-do-event-handlers-stop-garbage-collection-from-occurring
            //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.
    });
}
Ballerina answered 10/11, 2016 at 15:25 Comment(0)
B
4

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;

}
Belfast answered 6/8, 2017 at 13:24 Comment(0)
K
3

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>
Kwa answered 13/1, 2017 at 10:10 Comment(0)
T
2

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.

Tsosie answered 13/12, 2011 at 14:37 Comment(0)
D
2

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);
Diphenyl answered 15/7, 2020 at 13:11 Comment(1)
Thanks. I actually needed a quick fixChappelka

© 2022 - 2024 — McMap. All rights reserved.