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