使用 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
为我们提供了 CollectionChanged
Event 并实现了 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
。