ObservableCollection和项PropertyChanged

58

我看到很多关于这个问题的讨论,但可能我太新手了,无法理解。如果我有一个可观察的集合,它是“PersonNames”的集合,就像msdn示例中一样(http: //msdn.microsoft.com/en-us/library/ms748365.aspx),如果添加或删除了PersonName等,我会得到对视图的更新。我还想在更改PersonName中的属性时也能更新视图,例如更改名字。我可以为每个属性实现OnPropertyChanged,并使该类派生自INotifyPropertyChanged,这似乎按预期调用。

我的问题是,当属性更改不会导致ObservableCollection事件时,视图如何从ObservableCollection获取更新的数据?

这可能是非常简单的事情,但我为什么找不到示例,让我感到惊讶。有人能为我解释一下吗?或者有任何指向示例的指针,我将不胜感激。我们在当前的WPF应用程序中有多个地方出现这种情况,正在努力解决它。


“通常,负责显示数据的代码会向当前屏幕上显示的每个对象添加一个PropertyChanged事件处理程序。”

请问有人能给我一个例子说明这是什么意思吗?我的视图绑定到我的ViewModel,它具有一个ObservableCollection。该集合由支持PropertiesChanged事件的RowViewModel组成。但我无法弄清楚如何使集合更新自身,以便我的视图将得到更新。

6个回答

77

以下是如何附加/分离每个项的PropertyChanged事件。

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

3
这真美丽。我已经寻找了一段时间,它确实帮助了我。非常感谢。 - pqsk
2
已经有一段时间了,我找不到为这个示例抽象的源代码。我想我当时使用的是静态的WPF依赖属性。我认为这些函数为什么不能是每个实例的呢? - chilltemp
9
需要翻译的内容:No need to check that e.OldItems != null and e.NewItems != null?需要检查 e.OldItems 和 e.NewItems 是否为 null 吗? - Johan Larsson
4
重置事件时,我认为这种方法不起作用,因为它们不会填充旧项目。 - CodeHulk
1
@Maverik foreach 循环会抛出 NullReferenceException 异常,因此我们确实需要检查是否为 null。 - Alex Telon
显示剩余7条评论

27
我们在WPF聊天中写了这个:
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);
    }
}
  • 弱事件
  • 跟踪同一项多次添加到集合中
  • 它会将子元素的属性更改事件“冒泡”上来。
  • 这个静态类只是为了方便。

使用方法如下:

var listener = OcPropertyChangedListener.Create(yourCollection);
listener.PropertyChanged += (sender, args) => { //do you stuff}

非常好 - 我特别喜欢它使用弱事件来跟踪项目,因为这消除了许多取消订阅的复杂性,使其更有用。 - Reed Copsey
1
鉴于所有不同的解决方案,我认为这个是迄今为止最好的实现。干得好,约翰。 - Maverik
我来晚了。这对于父模型很好用,但不适用于导航属性,例如实现INotifyPropertyChanged的List模型属性。 - Aaron. S
我替换了CollectionChangedEventManager和PropertyChangedEventManager,因为在使用DependencyInjection(Unity)时无法正常工作。而改用常规的+= ChildPropertyChanged;确实有效。 - Coden
我正在尝试使用这个实现,但是在使用CollectionChangedEventManager时出现错误,提示当前上下文中不存在该对象,尽管我已经使用了System.Collections.Specialized命名空间。这是在.NET 6中发生的。 - Álvaro García

14

Bill,

我相信你现在已经找到了解决你问题的方法,但是我发布这篇文章是为了帮助其他遇到同样问题的人。你可以使用这个类来替换ObservableCollections,它是由实现INotifyPropertyChanged接口的对象组成的集合。它有点过于严格,因为它认为列表需要重置而不是找到已更改的属性/项,但对于小型列表,性能损失应该是微不足道的。

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

        }

    }
}

这很好。但幸运的是,您不必重置集合,您也可以通过以下方式进行替换: var replace = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, this.Items.IndexOf((T)sender)); this.OnCollectionChanged(replace); - infografnet
@Marc-Ziss 这是一个不错的解决方案。 - likebobby
@infografnet 我无法使用Replace使其正常工作。这可能是因为oldItem和newItem都是sender的原因吗?我已确保属性更改不会检查值是否与之前相同。 - likebobby
1
@BobbyJ,是的,你说得对。在这种情况下,oldItem将与newItem相同。但这不应该有影响。只需尝试在回调函数中跳过检查oldItem。如果(e.Action == NotifyCollectionChangedAction.Replace){使用e.NewItems做一些事情,不要检查e.OldItems;} - infografnet

4

不要使用ObservableCollection,而是使用BindingList<T>。下面的代码展示了一个DataGrid绑定到一个List和绑定到项的属性。

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

我不知道有一个DispatcherTimer可以让你更新UI线程。谢谢。 - Alberto Rechy

4

正如您所发现的那样,没有一种集合级别的事件可以指示集合中的项的属性已更改。通常,负责显示数据的代码会向当前屏幕上显示的每个对象添加PropertyChanged事件处理程序。


谢谢。我正在使用WPF,并且有一个DataGrid,其ItemsSource在XAML中绑定到ObservableCollection。因此,我需要在我的ViewModel中的某个位置添加代码来处理PropertyChanged事件,以便View知道更新DataGrid?然后,我是否必须将项目从集合中删除并添加以使View更新它?这似乎是违反直觉的(但这并不意味着它不正确 :))。 - Bill Campbell
1
如果ObservableCollection中的元素实现了INotifyPropertyChanged(或者是DependencyObject),那么DataGrid会自动完成此操作。 - Goblin

2
以下是代码,它简要解释了@Stack的答案,并展示了BindingList如何观察它是否有项目更改并显示ObservableCollection不会观察项目内部的更改。
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));
            }
        }
    }
}

它对我来说不起作用。和Observable Collection一样。我有什么遗漏吗? - SANDEEP MACHIRAJU

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接