ObservableCollection<T>

佚名 / 2023-08-04 / 原文

public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged

上述代码是ObservableCollection类的声明,首先关注到它只继承了一个类Collection,所以本质上它就是一个集合。其次,它实现了接口INotifyPropertyChanged,能够对外通知无参属性Count和有参属性Item[]发生变化。

OnPropertyChanged("Count");
OnPropertyChanged("Item[]");

所以,View可以使用一个TextBlock绑定到Count,实时显示ObservableCollection元素数量的动态变化。

<TextBlock Text="{Binding ObservableProducts.Count}" />

View可以将复杂控件绑定到ObservableCollection的位于某个索引处的元素,在Model的setter中发布

Content="{Binding Products[0].Model}"

OnPropertyChanged("Item[]");

另外,ObservableCollection还实现了接口INotifyCollectionChanged,当集合中的元素数量,元素的位置发生变化时(Add,Remove,Replace,Move,Reset)

// 捕获集合添加元素事件
// args.NewItems是本次添加的元素总和
// args.NewStartingIndex是插入的索引起始位。假设ObservableProducts共有5个元素,现追加若干元素,则args.NewStartingIndex是5,若在第2个元素后插入若干元素,则args.NewStartingIndex是2. args.OldItems恒为null,args.OldStartingIndex恒为-1.
ObservableProducts.CollectionChanged += (sender, args) =>
{
    if (args.Action == NotifyCollectionChangedAction.Add)
    {
        var productsAdded = args.NewItems.Cast<Product>();
        var index = args.NewStartingIndex;
        var c = args.OldStartingIndex; // -1
        var d = args.OldItems; // null
    }
};

// 捕获集合删除元素事件
// args.OldItems是本次删除元素的集合,args.OldStartingIndex是删除的第一个元素的起始索引。假设RemoveAt(1),则args.OldStartingIndex是1.
// args.NewItems恒为null, args.NewStartingIndex恒为-1
ObservableProducts.CollectionChanged += (sender, args) =>
{
    if (args.Action == NotifyCollectionChangedAction.Remove)
    {
        var d = args.NewItems; // null
        var a = args.NewStartingIndex; // -1
        var b = args.OldStartingIndex;
        var c = args.OldItems;
    }
};

小结: ObservableCollection相较于普通集合如Array,List,ArrayList,HashSet等,它实现了增删清空时的通知功能,其次Count.

ObservableCollection和UI线程

可以在任意线程上增删ObservableCollection中的元素,必要情况下注意需要保证线程安全,因为其实现了接口ICollection,所以可以利用lock(this.SyncRoot)这种简捷方式实现线程安全。
但是一旦ObservableCollection或其CollectionView绑定到ItemsSource,它的CollectionChanged会被列表控件注册,事件处理程序里面涉及读写WPF控件,又因为ObservableCollection的Add、Remove、Clear等会改变集合元素数量的API内部都会发布CollectionChanged,所以导致ObservableCollection几乎必须在其关联的列表控件所在的UI线程被调用,否则会抛出异常:System.NotSupportedException:“该类型的 CollectionView 不支持从调度程序线程以外的线程对其 SourceCollection 进行的更改。”
因为上面的原因,我们总是在UI线程访问ObservableCollection,UI线程是单线程模型,天生的线程安全,避免了多线程读写问题,综上,ObservableCollection总是在UI线程上保证线程安全的被访问。

CollectionView、ObservableCollection、Repository三者元素之间的关系

Repository的元素浅克隆到ObservableCollection,ObservableCollection再浅克隆到CollectionView,只发生了引用地址的复制,堆中实例的数目并未变多,堆中的某一个实例被3个引用变量指向。

ObservableCollection<Product> observableProducts = new ObservableCollection<Product>(Repository.Instance);
ListCollectionView lcv = CollectionViewSource.GetDefaultView(observableProducts) as ListCollectionView;

遍历输出3个集合元素的GetHashCode(),Product并未重写GetHashCode(),所以实例的GetHashCode()具有唯一性,GetHashCode相同,一定是同一个实例,GetHashCode()不同,一定不是同一个实例。

image

增删技巧

一定要通过ObservableCollection集合增删元素,不要在CollectionView集合上执行增删,原因是前者具有以下优点:

  • ObservableCollection增删后,能够自动激发它拥有的所有视图进行更新,包括重新应用过滤、排序、分组等规则。
  • ObservableCollection增删后,发布CollectionChanged事件,外界捕获集合增删的元素。
  • ObservableCollection所有元素的集中地和发源地,蕴藏着所有元素,在开发中经常需要取所有元素进行持久化,显然这个集合是最佳的地方,所以应当在此数据源精准增删。但是CollectionView并不包含所有的元素,是二手贩子,增删要直捣黄龙,不应再副本上瞎搞。
ObservableCollection添加了已含有的引用会怎样?
observableProducts.Add(observableProducts[0]);

执行上面代码后,ObservableCollection集合和CollectionView集合的元素数量都会+1,UI上也会多出一个ItemsControlItem,一切似乎没什么不同。但是要注意,集合尾部元素和第一个元素指向同一个堆中的实例,假设我们在UI选中第一个元素对应的ItemsControlItem,然后修改其对应的源集合元素的属性,我们会发现,尾部元素和第一个元素都发生了变化,而我们期望的是只有第一个元素的属性的值的发生变化。所以,在添加元素的时候,应当new一个新的实例,将新实例的引用添加到ObservableCollection中。Remove时时正常的,当我们删除第一个元素,尾部元素并不会同步消失。
通过操作视图增添新元素的场景举例。

  1. 在View上,选中某一行,然后基于此行信息增加一行,在MenuContext
  2. 在View上,点击某一行的增加一行
  3. 在外界边框输入一行数据,点击录入按钮

删除

增删时,如何同步仓库集合?

ObservableCollection增删时,会同步增删CollectionView,但是不会同步增删Repository。需要监听ObservableCollection的CollectionChanged事件来同步增删Repository。

通过选择删除,通过按钮删除,通过
判等逻辑一定要是引用地址判等。

// 需求
// 读取数据库数据显示到列表,增删改反应到数据库
// 增,删,改,所有操作做完之后,点提交按钮再一次性把本次的所有修改反馈到数据库
// 增的不在删除集合里面的要写入
// 删的不在增集合里面的要删除
// 改的不在删除集合且不在增集合里面的要更新

public class ObservableCollectionEx<T> : ObservableCollection<T>
{
    public ObservableCollectionEx()
    {
    }

    public ObservableCollectionEx(List<T> list) : this(list as IEnumerable<T>)
    {
    }

    public ObservableCollectionEx(IEnumerable<T> items) : base(items)
    {
        foreach (var item in items)
        {
            RegisterPropertyChanged(item);
        }
    }

    public new void Add(T item)
    {
        base.Add(item);
        RegisterPropertyChanged(item);
    }

    public new void Insert(int index, T item)
    {
        base.Insert(index, item);
        RegisterPropertyChanged(item);
    }

    public new void Remove(T item)
    {
        base.Remove(item);
        UnregisterPropertyChanged(item);
    }

    public new void RemoveAt(int index)
    {
        base.RemoveAt(index);

        UnregisterPropertyChanged(this[index]);
    }

    private void Inpc_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        SetItem(this.IndexOf((T)sender), (T)sender);
    }

    private void RegisterPropertyChanged(T item)
    {
        if (item is INotifyPropertyChanged inpc)
        {
            inpc.PropertyChanged -= Inpc_PropertyChanged;
            inpc.PropertyChanged += Inpc_PropertyChanged;
        }
    }

    private void UnregisterPropertyChanged(T item)
    {
        if (item is INotifyPropertyChanged inpc)
        {
            inpc.PropertyChanged -= Inpc_PropertyChanged;
        }
    }
}