使用 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