【问题标题】:Notify ObservableCollection when Item changes当 Item 改变时通知 ObservableCollection
【发布时间】:2012-01-19 09:42:06
【问题描述】:

我在这个链接上找到了

ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)

一些技术来通知 Observablecollection 项目已更改。此链接中的 TrulyObservableCollection 似乎是我正在寻找的。​​p>

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);
    }
}

但是当我尝试使用它时,我没有收到关于收藏的通知。我不确定如何在我的 C# 代码中正确实现这一点:

XAML:

    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MyItemsSource, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <DataGrid.Columns>
            <DataGridCheckBoxColumn Binding="{Binding MyProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        </DataGrid.Columns>
    </DataGrid>

视图模型:

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);
        }
    }
}

当我运行程序时,我将 3 个复选框设置为 false、true、false,就像在属性初始化中一样。 但是当我更改其中一个复选框的状态时,程序会通过 item_PropertyChanged 但从未在 MyItemsSource 属性代码中。

【问题讨论】:

  • 您是否尝试过在调试器中跟踪 RaisePropertyChangedEvent 方法?换句话说,控件是否进入 if 块?
  • ObservableCollection 不应该在集合中某个项目的属性更改时引发CollectionChanged。因为收藏没有改变。我无法弄清楚您实际上想要做什么,但我认为您可能会从查看 ContinuousLINQ 中受益——clinq.codeplex.com
  • 我使用了代码。问题是您取消订阅所有旧项目的属性更改事件。我评论了那部分,一切都很顺利。
  • 而不是 TrulyObservableCollection 只需使用 System.ComponentModel.BindingList<T>

标签: c# wpf collections observablecollection inotifypropertychanged


【解决方案1】:

对此的一个简单解决方案是替换 ObservableCollection 中正在更改的项目,该项目通知集合已更改的项目。在下面的示例代码 sn-p 中,Artists 是 ObservableCollection,artist 是 ObservableCollection 中类型的一个项目:

    var index = Artists.IndexOf(artist);
    Artists.RemoveAt(index);
    artist.IsFollowed = true; // change something in the item
    Artists.Insert(index, artist);

【讨论】:

  • 谢谢。我实际上需要一个快速修复
【解决方案2】:

这里的所有解决方案都是正确的,但是它们缺少一个重要的场景,即使用方法Clear(),它没有在NotifyCollectionChangedEventArgs对象中提供OldItems

这是完美的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;

}

【讨论】:

    【解决方案3】:

    一个简单的解决方案是使用 BindingList&lt;T&gt; 而不是 ObservableCollection&lt;T&gt; 。实际上 BindingList 中继项目更改通知。因此,对于绑定列表,如果项目实现了接口INotifyPropertyChanged,那么您可以简单地使用the ListChanged event 获取通知。

    另请参阅this SO 答案。

    【讨论】:

    • 正是我想要的。不记得类型的名称。
    • 我花了几个小时尝试上述示例并失败了。然后我找到了这个答案,它立即奏效了!这应该有更多的投票,因为它提供了一个快速简单的解决方案!在链接的答案中,您会发现一些缺点,但就我而言,它解决了问题。
    【解决方案4】:

    我知道已经晚了,但也许这对其他人有帮助。我创建了一个类NotifyObservableCollection,它解决了当项目的属性发生变化时缺少对项目本身的通知的问题。用法很简单,如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);
        }
    

    在添加或删除项目时,该类将项目PropertyChanged 事件转发到集合PropertyChanged 事件。

    用法:

    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; }
    }
    

    如果现在DataItems 中某个项目的属性发生更改,则以下 xaml 将收到通知,尽管它绑定到 Parameters[0] 或项目本身,但项目的更改属性 Value 除外(转换器位于触发器在每次更改时都被称为可靠)。

    <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>
    

    【讨论】:

      【解决方案5】:

      您可以使用扩展方法以通用方式获取有关集合中项目属性更改的通知。

      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.
          });
      }
      

      【讨论】:

        【解决方案6】:

        您评论为// Code to trig on item change... 的位置只会在集合对象发生更改时触发,例如当它被设置为新对象或设置为空时。

        使用您当前的 TrulyObservableCollection 实现,要处理集合的属性更改事件,请将某些内容注册到 MyItemsSourceCollectionChanged 事件

        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
        }
        

        就我个人而言,我真的不喜欢这种实现方式。您正在引发一个CollectionChanged 事件,表明整个集合已被重置,只要属性发生更改。当然,只要集合中的项目发生更改,它就会使 UI 更新,但我发现这对性能很不利,而且它似乎没有办法识别哪些属性发生了变化,这是关键信息之一我在PropertyChanged 上做某事时通常需要。

        我更喜欢使用常规的ObservableCollection 并将PropertyChanged 事件连接到CollectionChanged 上的项目。如果您的 UI 正确绑定到 ObservableCollection 中的项目,则您不需要告诉 UI 在集合中项目的属性发生更改时进行更新。

        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();
        }
        

        【讨论】:

        • 谢谢!您对性能是正确的,因为 dataGrid 在复选框单击时闪烁。让第一个添加的项目在构造函数中订阅Propertychange只是一件事,MyItemsSource必须在订阅CollectionChange事件后初始化
        • 如果您将 XAML 直接绑定到项目本身而不是项目上的属性,这将不起作用。在我的情况下(如果您参考他的示例),将 XAML 中的一行更改为如下所示:&lt;DataGridCheckBoxColumn Binding="{Binding}"/&gt; 任何人都有针对这种情况的解决方案?
        • @FrankLiu An ObservableCollection.CollectionChanged 仅在集合本身发生变化时触发——要么更改为新集合,要么添加新项目,要么删除项目。当集合中的项目触发PropertyChange 通知时,它 触发。此处的代码将PropertyChanged 事件处理程序连接到每个项目,以在PropertyChanged 被触发时触发CollectionChanged
        • @Rachel 再次感谢您的回复。我通过在 MyType_PropertyChanged 事件处理程序中调用 OnPropertyChanged("MyCollection") 为我的 ObservableCollection 引发了 PropertyChanged 事件。更新集合内项目的属性时,确实会引发该事件。但是 ItemSource 绑定到 MyCollection 的 ItemsControl 仍未更新/刷新。在我看来,引发 ObservableCollection 的 PropertyChanged 事件不会引发 ItemsControl 正在侦听的 ObservableCollection.CollectionChanged 事件。
        • @Rachel“我真的不喜欢这个实现......” - 我已经put up an answer 提出了TrulyObservableCollection&lt;&gt; 的问题,该问题的解决方案稍微复杂一点,@ 987654340@。我认为它解决了这个版本的一些问题。
        【解决方案7】:

        我通过静态动作解决了这个案例


        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
                    };
        }
        

        【讨论】:

        • 对于要检查的项目的唯一实例在该 ObservableCollection 中的情况,这是一个很好的选择。
        【解决方案8】:

        ObservableCollection 及其衍生物在内部引发其属性更改。仅当您将新的 TrulyObservableCollection&lt;MyType&gt; 分配给 MyItemsSource 属性时,才应触发 setter 中的代码。也就是说,它应该只发生一次,从构造函数开始。

        从那时起,您将收到来自集合的属性更改通知,而不是来自视图模型中的设置器。

        【讨论】:

          猜你喜欢
          • 2017-06-14
          • 2017-12-23
          • 2016-08-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-10-28
          • 2020-11-16
          • 2014-07-17
          相关资源
          最近更新 更多