使用 INotifyPropertyChanged 和 INotifyCollectionChanged 绑定到对象集合

假设你有一个 ListView,它应该显示在 ViewModel 属性 Users 属性下列出的每个 User 对象,其中用户对象的属性可以以编程方式更新。

<ListView ItemsSource="{Binding Path=Users}" >
    <ListView.ItemTemplate>
        <DataTemplate DataType="{x:Type models:User}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Margin="5,3,15,3" 
                         Text="{Binding Id, Mode=OneWay}" />
                <TextBox Width="200"
                         Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Delay=450}"/>
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

尽管为 User 对象正确实施了 INotifyPropertyChanged

public class User : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private int _id;
    private string _name;

    public int Id
    {
        get { return _id; }
        private set
        {
            if (_id == value) return;
            _id = value;
            NotifyPropertyChanged();
        }
    }
    public string Name
    {
        get { return _name; }
        set
        {
            if (_name == value) return;
            _name = value;
            NotifyPropertyChanged();
        }
    }

    public User(int id, string name)
    {
        Id = id;
        Name = name;
    }

    private void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

并为你的 ViewModel 对象

public sealed class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private List<User> _users;
    public List<User> Users
    {
        get { return _users; }
        set
        {
            if (_users == value) return;
            _users = value;
            NotifyPropertyChanged();
        }
    }
    public MainWindowViewModel()
    {
        Users = new List<User> {new User(1, "John Doe"), new User(2, "Jane Doe"), new User(3, "Foo Bar")};
    }

    private void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

如果以编程方式对用户进行更改,则 UI 不会更新。

这只是因为你只在 List 实例上设置了 INotifyPropertyChanged。仅当你完全重新实例化 List 时,如果 Element 的一个属性发生更改,你的 UI 将更新。

// DO NOT DO THIS
User[] userCache = Users.ToArray();
Users = new List<User>(userCache);

然而,这对于性能来说非常烦人且难以置信。
如果你有一个包含 100'000 元素的列表,同时显示用户的 ID 和名称,则将有 200'000 个 DataBinding,每个都必须重新创建。每当对任何内容进行更改时,这会导致用户明显滞后。

要部分解决此问题,你可以使用 System.ComponentModel.ObservableCollection<T> 而不是 List<T>

private ObservableCollection<User> _users;
public ObservableCollection<User> Users
{
    get { return _users; }
    set
    {
        if (_users == value) return;
        _users = value;
        NotifyPropertyChanged();
    }
}

ObservableCollection 为我们提供了 CollectionChangedEvent 并实现了 INotifyPropertyChanged 本身。根据 MSDN ,事件将会上升,“[..] 添加删除更改移动项目刷新整个列表时 ”。
然而,你将很快意识到,使用 .NET 4.5.2 和之前的版本,如果此处讨论的集合中元素的属性发生更改,则 ObservableCollection 将不会引发 CollectionChanged 事件。

根据这个解决方案,我们可以简单地实现我们自己的 TrulyObservableCollection<T> 而不需要 INotifyPropertyChanged 约束,因为 T 拥有我们需要的一切,并且暴露了 T 实现了 INotifyPropertyChanged

/*
 * Original Class by Simon @StackOverflow http://stackoverflow.com/a/5256827/3766034
 * Removal of the INPC-Constraint by Jirajha @StackOverflow 
 * according to to suggestion of nikeee @StackOverflow http://stackoverflow.com/a/10718451/3766034
 */
public sealed class TrulyObservableCollection<T> : ObservableCollection<T>
{
    private readonly bool _inpcHookup;
    public bool NotifyPropertyChangedHookup => _inpcHookup;

    public TrulyObservableCollection()
    {
        CollectionChanged += TrulyObservableCollectionChanged;
        _inpcHookup = typeof(INotifyPropertyChanged).GetTypeInfo().IsAssignableFrom(typeof(T));
    }
    public TrulyObservableCollection(IEnumerable<T> items) : this()
    {
        foreach (var item in items)
        {
            this.Add(item);
        }
    }

    private void TrulyObservableCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (NotifyPropertyChangedHookup && e.NewItems != null && e.NewItems.Count > 0)
        {
            foreach (INotifyPropertyChanged item in e.NewItems)
            {
                item.PropertyChanged += ItemPropertyChanged;
            }
        }
        if (NotifyPropertyChangedHookup && e.OldItems != null && e.OldItems.Count > 0)
        {
            foreach (INotifyPropertyChanged item in e.OldItems)
            {
                item.PropertyChanged -= ItemPropertyChanged;
            }
        }
    }
    private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
        OnCollectionChanged(args);
    }
}

并在我们的 ViewModel 中将我们的属性 Users 定义为 TrulyObservableCollection<User>

private TrulyObservableCollection<string> _users;
public TrulyObservableCollection<string> Users
{
    get { return _users; }
    set
    {
        if (_users == value) return;
        _users = value;
        NotifyPropertyChanged();
    }
}

现在,一旦集合中元素的 INPC 属性发生变化,我们的 UI 就会收到通知,而无需重新创建每个单独的 Binding