Search code examples
c#xamarindata-bindingobservablecollection

C#, Xamarin Forms - ObservableCollection of custom Object updates but does not fire property changed events


I have a collection that gets populated with produce. Once the collection is populated, a BindableLayout/DataTemplate bound to a StackLayout will display the items and the user will be prompted with the option to change the Price property of a stock item by typing into an Entry.

A user can type into the provided Entry box to change Price property of each StockInfo object in the collection, and the change WILL SUCCESSFULLY be applied to the Observable Collection, BUT it WILL NOT fire the setter/property changed event of the Observable Collection.

I need the property changed event to fire so that I can effectively execute other parts of my code, but since it won't the fire setter or property changed of the collection, it never gets the chance to tell other parts of my code to do things.

namespace Test
{
    public class Testing : BaseContentPage, INotifyPropertyChanged
    {
        public class StockInfo : BaseContentPage, INotifyPropertyChanged
        {
            private string description;
            public string Description
            {
                get => description;
                set
                {
                    description = value; 
                    OnPropertyChanged();
                }
            }

            private int price;
            public int Price
            {
                get => price;
                set
                {
                    price = value; 
                    OnPropertyChanged();
                }
            }
        }

        private ObservableCollection<StockInfo> stockItems = new ObservableCollection<StockInfo>();
        public ObservableCollection<StockInfo> StockItems
        {
            get => stockItems;
            set
            {
                stockItems = value;
                OnPropertyChanged();
                OnPropertyChanged("SumPrices");
            }
        }

        public double SumPrices
        {
            get
            {
                return StockItems.Sum(p => p.Price);
            }
        }

        DataTemplate StockTemplate = new DataTemplate(() =>
        {
            return new StackLayout
            {
                Orientation = StackOrientation.Horizontal,

                Children =
                {
                    new Entry
                    {
                    }.Bind(Entry.TextProperty, path: "Description")

                    ,

                    new Entry
                    {
                        Keyboard = Keyboard.Numeric
                    }.Bind(Entry.TextProperty, path: "Price")
                }
            };
        });


        public Testing()
        {
            BindingContext = this;

            StockItems.Add(new StockInfo { Description = "Milk", Price = 20 });
            StockItems.Add(new StockInfo { Description = "Cheese", Price = 15 });

            Content = new StackLayout
            {
                Children =
                {
                    new StackLayout
                    {
                    }.Invoke(layout => BindableLayout.SetItemTemplate(layout, StockTemplate))
                    .Bind(BindableLayout.ItemsSourceProperty, path: "StockItems")

                    ,

                    new Label
                    {
                    }.Bind(Label.TextProperty, path: "SumPrices")
                }
            };
        }
    }
}

If I put a debugger stop line inside the get/set of the "Description" property in the StockInfo class and then type in the Entry, the debugger will pick it up and stop the program for debugging.

But if I put a debugger stop on a line some where in the set/get of the Observable Collection, the program will not stop inside of it.

*** Edits Below ***

I modified the code so that StockInfo now has a property that includes the price of a product. I also added a variable called SumPrices which will return the Sum of Price within StockItems using LINQ. The first time the page loads, the sum is calculated and the result is correct, but if I change the Entry box that the property is bound to for each object, it has no effect and the SumPrices variable never changes.

Ideally, I'd simply like for the Observable Collection to fire its setter/property change events whenever an Object's property within the collection is changed.


Solution

  • New Update Here

    The Setter method of ObservableCollection will not be fired when a property of an item changes. Here is a related issue on StackOverflow: ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged). Bob Sammers abstract and define a new FullyObservableCollection class and put forward a pretty robust solution, including some of the techniques in other answers. This new class could get notified when a property of item has been changed. I have tested it and worked well.

    Simply used it like the following code:

    private FullyObservableCollection<StockInfo> stockItems = new FullyObservableCollection<StockInfo>();
    public FullyObservableCollection<StockInfo> StockItems
    {
        get => stockItems;
        set
        {
            stockItems = value;
            OnPropertyChanged();
        }
    }
    
    public Testing ()
        {
            ...
            StockItems.ItemPropertyChanged += StockItems_ItemPropertyChanged;
            ...
        }
    
        private void StockItems_ItemPropertyChanged(object sender, ItemPropertyChangedEventArgs e)
        {
            OnPropertyChanged(nameof(SumPrices));
        }
    

    Another workaround is what i've suggested in my previous answer, which is using TextChanged event handler.

    In Datatemplate, add an EventHandler for entry:

    new Entry
    {
    }.Bind(Entry.TextProperty, path: "Description",BindingMode.TwoWay)
    .Invoke(entry=>entry.TextChanged+=Entry_TextChanged)
    
    
    private void Entry_TextChanged(object sender, TextChangedEventArgs e)
        {
            OnPropertyChanged(nameof(SumPrices));
        }
    

    For more info, you could refer to Xamarin.Forms - CollectionView sample

    Hope it works for you.