BindingList的應用與改進
在編寫UI的過程中,我們通常使用ObservableCollection來監(jiān)聽列表的變化。然而,ObservableCollection只能在添加/移動/移除元素時通知界面,這意味著元素內(nèi)部更改時,ObservableCollection是無法通知的
如果需要監(jiān)聽列表元素內(nèi)部的更改,可以使用System.ComponentModel.BindingList。
BindingList作用是將列表中元素內(nèi)部的更改"轉(zhuǎn)發(fā)"到外部。由于需要監(jiān)聽每個元素內(nèi)部的屬性更改,BindingList中的所有元素必須實現(xiàn)INotifyPropertyChanged
使用
現(xiàn)有Item類如下:
public partial class Item : ObservableObject
{
[ObservableProperty]
public partial string? Name { get; set; }
[ObservableProperty]
public partial int Value { get; set; }
}
有Items列表中存儲多個Item,如果需要計算列表中所有Value的總和,我們就可以使用BindingList
[ObservableProperty]
public partial BindingList<Item> Items { get; set; } = [];
public int TotalValue => Items.Sum(i => i.Value);
然而修改Items中元素后,TotalValue并沒有被更新,這是為什么呢?
事實上,BindingList并不能主動通知TotalValue屬性。但它提供了十分強大的ListChanged事件,它在添加/刪除元素或元素內(nèi)部更改時均會觸發(fā)(會根據(jù)更改類型會在ListChangedEventArgs中提供不同的ListChangedType),這是ObservableCollection無法做到的
public enum ListChangedType
{
Reset,// 清空列表或列表行為變化(AllowNew/AllowEdit/AllowRemove發(fā)生改變)
ItemAdded,// 添加元素
ItemDeleted// 刪除元素
ItemMoved,// 移動元素
ItemChanged,// 元素內(nèi)部屬性更改
// BindingList未使用下面三個成員
PropertyDescriptorAdded,
PropertyDescriptorDeleted,
PropertyDescriptorChanged
}
我們可以訂閱此事件并完成對TotalValue的通知
public MainViewModel()
{
// 此處OnPropertyChanged為MVVM工具包中ObservableObject的代碼,可替換為PropertyChanged?.Invoke()
Items.ListChanged += (s, e) => OnPropertyChanged(nameof(TotalValue));
}
現(xiàn)在,TotalValue在元素更改時就會重新計算,可直接用于單向綁定
缺陷以及解決方案
在Avalonia測試時,會發(fā)現(xiàn)一個很奇怪的現(xiàn)象:如果將BindingList作為列表控件的ItemSource使用,在添加/刪除元素時,盡管TotalValue會被正確更新,但列表沒有任何變化。同時,Count屬性也沒有得到正確通知

查看Avalonia中ItemSourceView的代碼后發(fā)現(xiàn),它只通過INotifyCollectionChanged的CollectionChanged事件來刷新列表,而BindingList并未實現(xiàn)和INotifyCollectionChanged接口,這也就是為什么BindingList無法正確通知UI
同時,BindingList也未實現(xiàn)INotifyPropertyChanged,造成Count屬性未更新
WinUI 3中,列表未刷新但
Count屬性能更新,可能是不同UI框架實現(xiàn)的問題
private protected void SetSource(IEnumerable source)
{
...
if (_listening && _source is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this);
}
現(xiàn)在解決方法就很簡單了:繼承BindingList,實現(xiàn)這兩個接口并在添加/移除元素進行通知即可。
完整代碼如下:
public class ObservableBindingList<T> : BindingList<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public event PropertyChangedEventHandler? PropertyChanged;
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
CollectionChanged?.Invoke(index, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));// 通知集合索引器的變化(通過索引器綁定列表第幾項時使用)
}
protected override void RemoveItem(int index)
{
var item = this[index];
base.RemoveItem(index);
CollectionChanged?.Invoke(index, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
}
}
使用:
[ObservableProperty]
public partial ObservableBindingList<Item> Items { get; set; } = [];
public int TotalValue => Items.Sum(i => i.Value);
public MainViewModel()
{
// 此處OnPropertyChanged為MVVM工具包中ObservableObject的代碼,可替換為PropertyChanged?.Invoke()
Items.ListChanged += (s, e) => OnPropertyChanged(nameof(TotalValue));
}
現(xiàn)在,增刪(移動)元素/修改元素內(nèi)部的值均可正確通知界面


浙公網(wǎng)安備 33010602011771號