劉漪 發表於 2025-9-29 12:26:00

BindingList的应用与改进

<p>在编写UI的过程中,我们通常使用<code>ObservableCollection</code>来监听列表的变化。然而,<code>ObservableCollection</code>只能在<strong>添加/移动/移除元素</strong>时通知界面,这意味着元素内部更改时,<code>ObservableCollection</code>是无法通知的</p>
<p>如果需要监听列表元素内部的更改,可以使用<code>System.ComponentModel.BindingList</code>。</p>
<p><code>BindingList</code>作用是将列表中元素内部的更改"转发"到外部。由于需要监听每个元素内部的属性更改,<strong><code>BindingList</code>中的所有元素必须实现<code>INotifyPropertyChanged</code></strong></p>
<h2 id="使用">使用</h2>
<p>现有<code>Item</code>类如下:</p>
<pre><code class="language-csharp">public partial class Item : ObservableObject
{
   
    public partial string? Name { get; set; }

   
    public partial int Value { get; set; }
}
</code></pre>
<p>有<code>Items</code>列表中存储多个<code>Item</code>,如果需要计算列表中所有<code>Value</code>的总和,我们就可以使用<code>BindingList</code></p>
<pre><code class="language-csharp">
public partial BindingList&lt;Item&gt; Items { get; set; } = [];

public int TotalValue =&gt; Items.Sum(i =&gt; i.Value);
</code></pre>
<p>然而修改<code>Items</code>中元素后,<code>TotalValue</code>并没有被更新,这是为什么呢?</p>
<p>事实上,<code>BindingList</code>并不能主动通知<code>TotalValue</code>属性。但它提供了十分强大的<code>ListChanged</code>事件,它在添加/删除元素或元素内部更改时均会触发(会根据更改类型会在<code>ListChangedEventArgs</code>中提供不同的<code>ListChangedType</code>),这是<code>ObservableCollection</code>无法做到的</p>
<pre><code class="language-csharp">public enum ListChangedType
{
        Reset,// 清空列表或列表行为变化(AllowNew/AllowEdit/AllowRemove发生改变)
        ItemAdded,// 添加元素
        ItemDeleted// 删除元素
        ItemMoved,// 移动元素
        ItemChanged,// 元素内部属性更改

        // BindingList未使用下面三个成员
        PropertyDescriptorAdded,
        PropertyDescriptorDeleted,
        PropertyDescriptorChanged
}
</code></pre>
<p>我们可以订阅此事件并完成对<code>TotalValue</code>的通知</p>
<pre><code class="language-csharp">public MainViewModel()
{
    // 此处OnPropertyChanged为MVVM工具包中ObservableObject的代码,可替换为PropertyChanged?.Invoke()
    Items.ListChanged += (s, e) =&gt; OnPropertyChanged(nameof(TotalValue));
}
</code></pre>
<p>现在,TotalValue在元素更改时就会重新计算,可直接用于单向绑定</p>
<h2 id="缺陷以及解决方案">缺陷以及解决方案</h2>
<p>在Avalonia测试时,会发现一个很奇怪的现象:如果将<code>BindingList</code>作为列表控件的<code>ItemSource</code>使用,在添加/删除元素时,尽管<code>TotalValue</code>会被正确更新,但列表没有任何变化。同时,<code>Count</code>属性也没有得到正确通知</p>
<p><img src="https://img2024.cnblogs.com/blog/3275657/202509/3275657-20250929122350232-1460853265.gif" alt="video_01" loading="lazy"></p>
<p>查看Avalonia中ItemSourceView的代码后发现,它只通过<code>INotifyCollectionChanged</code>的<code>CollectionChanged</code>事件来刷新列表,而<code>BindingList</code>并未实现和<code>INotifyCollectionChanged</code>接口,这也就是为什么<code>BindingList</code>无法正确通知UI</p>
<p>同时,<code>BindingList</code>也未实现<code>INotifyPropertyChanged</code>,造成<code>Count</code>属性未更新</p>
<blockquote>
<p>WinUI 3中,列表未刷新但<code>Count</code>属性能更新,可能是不同UI框架实现的问题</p>
</blockquote>
<pre><code class="language-csharp">private protected void SetSource(IEnumerable source)
{
    ...
    if (_listening &amp;&amp; _source is INotifyCollectionChanged inccNew)
      CollectionChangedEventManager.Instance.AddListener(inccNew, this);
}
</code></pre>
<p>现在解决方法就很简单了:继承<code>BindingList</code>,实现这两个接口并在添加/移除元素进行通知即可。</p>
<p>完整代码如下:</p>
<pre><code class="language-csharp">public class ObservableBindingList&lt;T&gt; : BindingList&lt;T&gt;, 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;
      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[]"));
    }
}
</code></pre>
<p>使用:</p>
<pre><code class="language-csharp">
public partial ObservableBindingList&lt;Item&gt; Items { get; set; } = [];

public int TotalValue =&gt; Items.Sum(i =&gt; i.Value);

public MainViewModel()
{   
    // 此处OnPropertyChanged为MVVM工具包中ObservableObject的代码,可替换为PropertyChanged?.Invoke()
    Items.ListChanged += (s, e) =&gt; OnPropertyChanged(nameof(TotalValue));
}
</code></pre>
<p>现在,增删(移动)元素/修改元素内部的值均可正确通知界面</p>
<p><img src="https://img2024.cnblogs.com/blog/3275657/202509/3275657-20250929122401955-1952742419.gif" alt="video_02" loading="lazy"></p>
<h2 id="示例代码">示例代码</h2>
<p>BindingListTest</p><br><br>
来源:https://www.cnblogs.com/BettaFish/p/19118540
頁: [1]
查看完整版本: BindingList的应用与改进