You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# Xamarin.Forms:ObservableCollection内对象属性变更不触发集合属性变更事件

问题

我用ObservableCollection<StockInfo>存储农产品信息,通过BindableLayout和DataTemplate绑定到StackLayout展示条目。用户可以通过Entry修改StockInfo的Price属性,修改能成功作用到对象,但不会触发集合的setter和PropertyChanged事件。

我添加了SumPrices属性,用LINQ计算所有Price的总和,页面加载时计算正确,但修改Entry后总和不会更新。调试发现StockInfo的属性变更会触发自身的PropertyChanged,但集合的get/set断点根本不会命中。我需要集合在内部对象属性变更时,触发自身的PropertyChanged事件,来执行后续逻辑。

解决方案

核心原因

ObservableCollection默认只监听集合元素的增减/替换,不会监听元素内部属性的变化。所以修改元素属性时,集合本身不会触发PropertyChanged,自然也不会通知SumPrices更新。

具体实现步骤

  • 给每个StockInfo对象绑定PropertyChanged事件:当集合添加新元素时,订阅元素的PropertyChanged事件,在事件回调中触发SumPrices的PropertyChanged。
  • 处理集合元素移除的情况:如果有元素被移除,取消订阅事件,避免内存泄漏。
  • 优化绑定类型转换:Price是int类型,绑定到Entry.Text(字符串)时,添加类型转换器,避免类型错误。
修改后的完整代码
namespace Test
{
    public class Testing : BaseContentPage, INotifyPropertyChanged
    {
        public class StockInfo : INotifyPropertyChanged
        {
            private string _description;
            public string Description
            {
                get => _description;
                set
                {
                    _description = value;
                    OnPropertyChanged(nameof(Description));
                }
            }

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

            public event PropertyChangedEventHandler PropertyChanged;
            protected virtual void OnPropertyChanged(string propertyName)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private ObservableCollection<StockInfo> _stockItems;
        public ObservableCollection<StockInfo> StockItems
        {
            get => _stockItems;
            set
            {
                if (_stockItems != null)
                {
                    // 移除旧集合元素的事件订阅
                    foreach (var item in _stockItems)
                    {
                        item.PropertyChanged -= StockItem_PropertyChanged;
                    }
                }

                _stockItems = value;
                OnPropertyChanged(nameof(StockItems));
                
                if (_stockItems != null)
                {
                    // 给新集合元素订阅事件
                    foreach (var item in _stockItems)
                    {
                        item.PropertyChanged += StockItem_PropertyChanged;
                    }
                }

                OnPropertyChanged(nameof(SumPrices));
            }
        }

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

        DataTemplate StockTemplate = new DataTemplate(() =>
        {
            return new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                Children =
                {
                    new Entry().Bind(Entry.TextProperty, nameof(StockInfo.Description), Mode=BindingMode.TwoWay),
                    new Entry
                    {
                        Keyboard = Keyboard.Numeric
                    }.Bind(Entry.TextProperty, nameof(StockInfo.Price), Mode=BindingMode.TwoWay, 
                        Converter=new IntToStringConverter())
                }
            };
        });

        public Testing()
        {
            BindingContext = this;
            StockItems = new ObservableCollection<StockInfo>();
            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, nameof(StockItems)),

                    new Label().Bind(Label.TextProperty, nameof(SumPrices), StringFormat="总价: {0:C}")
                }
            };
        }

        private void StockItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(StockInfo.Price))
            {
                OnPropertyChanged(nameof(SumPrices));
            }
        }
    }

    // int与string双向转换转换器
    public class IntToStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value?.ToString();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (int.TryParse(value?.ToString(), out int result))
            {
                return result;
            }
            return 0;
        }
    }
}

额外说明

  • StockInfo是数据模型,不需要继承BaseContentPage,直接实现INotifyPropertyChanged即可。
  • 新增的IntToStringConverter解决了int类型与Entry.Text字符串的双向绑定转换问题,避免输入非数字时出错。
  • StockItems的setter中处理了旧集合的事件取消订阅,防止内存泄漏。
  • SumPrices添加了空值判断,避免集合为null时抛出异常。
  • 绑定指定Mode=BindingMode.TwoWay,确保输入修改能正确同步到数据源。

内容的提问来源于stack exchange,提问作者chiken nuget

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 19:25:50