连接 CollectionChanged 和 PropertyChanged (或者:为什么某些 WPF 绑定不刷新?)
WPF DataBindings 曾经让我很开心。我刚才偶然发现的一件事是,在某些时候它们只是没有按预期刷新。请看一下以下(相当简单)代码:
<Window x:Class="CVFix.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="40"></RowDefinition>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" Grid.Column="0"
ItemsSource="{Binding Path=Persons}"
SelectedItem="{Binding Path=SelectedPerson}"
x:Name="lbPersons"></ListBox>
<TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Path=SelectedPerson.Name, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>
XAML 背后的代码:
using System.Windows;
namespace CVFix
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ViewModel Model { get; set; }
public MainWindow()
{
InitializeComponent();
this.Model = new ViewModel();
this.DataContext = this.Model;
}
}
}
最后,这是 ViewModel 类:
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace CVFix
{
public class ViewModel : INotifyPropertyChanged
{
private PersonViewModel selectedPerson;
public PersonViewModel SelectedPerson
{
get { return this.selectedPerson; }
set
{
this.selectedPerson = value;
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("SelectedPerson"));
}
}
public ObservableCollection<PersonViewModel> Persons { get; set; }
public ViewModel()
{
this.Persons = new ObservableCollection<PersonViewModel>();
this.Persons.Add(new PersonViewModel() { Name = "Adam" });
this.Persons.Add(new PersonViewModel() { Name = "Bobby" });
this.Persons.Add(new PersonViewModel() { Name = "Charles" });
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
public class PersonViewModel : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return this.name; }
set
{
this.name = value;
if(this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
public override string ToString()
{
return this.Name;
}
public event PropertyChangedEventHandler PropertyChanged;
}
我想要发生的情况: 当我从列表框中选择一个条目并在文本框中修改其名称时,列表将更新以显示新值。
发生了什么:什么也没有。如果我有任何判断的话,这就是正确的行为。 我确保 SelectedItem 的 PropertyChanged 被触发,但这(当然)不会导致 CollectionChanged 被触发。
为了解决这个问题,我创建了一个具有公共 OnCollectionChanged 方法的 ObservableCollection 派生类,请参阅此处:
public class PersonList : ObservableCollection<PersonViewModel>
{
public void OnCollectionChanged()
{
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ));
}
}
我从 ViewModel 的构造函数访问此方法,如下所述:
public ViewModel()
{
PersonViewModel vm1 = new PersonViewModel()
{
Name = "Adam"
};
PersonViewModel vm2 = new PersonViewModel()
{
Name = "Bobby"
};
PersonViewModel vm3 = new PersonViewModel()
{
Name = "Charles"
};
vm1.PropertyChanged += this.PersonChanged;
this.Persons = new PersonList();
this.Persons.Add(vm1);
this.Persons.Add(vm2);
this.Persons.Add(vm3);
}
void PersonChanged(object sender, PropertyChangedEventArgs e)
{
this.Persons.OnCollectionChanged();
}
它可以工作,但它不是一个干净的解决方案。我的下一个想法是创建 ObservableCollection 的派生类,它在 CollectionChanged 处理程序中自动进行连接。
public class SynchronizedObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
foreach (INotifyPropertyChanged item in e.NewItems)
{
item.PropertyChanged += this.ItemChanged;
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
foreach (INotifyPropertyChanged item in e.OldItems)
{
item.PropertyChanged -= this.ItemChanged;
}
break;
}
}
base.OnCollectionChanged(e);
}
void ItemChanged(object sender, PropertyChangedEventArgs e)
{
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
问题是:有更好的方法吗?这真的有必要吗?
预先非常感谢您的任何意见!
WPF DataBindings used to make me happy. One thing I've stumbled over just now is that at some point they just don't refresh as intented. Please take a look at the following (fairly simple) code:
<Window x:Class="CVFix.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="40"></RowDefinition>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" Grid.Column="0"
ItemsSource="{Binding Path=Persons}"
SelectedItem="{Binding Path=SelectedPerson}"
x:Name="lbPersons"></ListBox>
<TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Path=SelectedPerson.Name, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>
The Code behind for the XAML:
using System.Windows;
namespace CVFix
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ViewModel Model { get; set; }
public MainWindow()
{
InitializeComponent();
this.Model = new ViewModel();
this.DataContext = this.Model;
}
}
}
Finally, here's the ViewModel classes:
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace CVFix
{
public class ViewModel : INotifyPropertyChanged
{
private PersonViewModel selectedPerson;
public PersonViewModel SelectedPerson
{
get { return this.selectedPerson; }
set
{
this.selectedPerson = value;
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("SelectedPerson"));
}
}
public ObservableCollection<PersonViewModel> Persons { get; set; }
public ViewModel()
{
this.Persons = new ObservableCollection<PersonViewModel>();
this.Persons.Add(new PersonViewModel() { Name = "Adam" });
this.Persons.Add(new PersonViewModel() { Name = "Bobby" });
this.Persons.Add(new PersonViewModel() { Name = "Charles" });
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
public class PersonViewModel : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return this.name; }
set
{
this.name = value;
if(this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
public override string ToString()
{
return this.Name;
}
public event PropertyChangedEventHandler PropertyChanged;
}
What I'd want to happen: As I select an entry from the ListBox and modify its Name in the TextBox, the list gets updated to display the new value.
What happens : nothing. And that is the correct behaviour, If I'm any judge.
I made sure that the SelectedItem's PropertyChanged is fired, but that does (of course) not cause CollectionChanged to be fired.
To fix this, I created an ObservableCollection-derived class that has a public OnCollectionChanged method, see here :
public class PersonList : ObservableCollection<PersonViewModel>
{
public void OnCollectionChanged()
{
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ));
}
}
I access this from the ViewModel's constructor, as described below:
public ViewModel()
{
PersonViewModel vm1 = new PersonViewModel()
{
Name = "Adam"
};
PersonViewModel vm2 = new PersonViewModel()
{
Name = "Bobby"
};
PersonViewModel vm3 = new PersonViewModel()
{
Name = "Charles"
};
vm1.PropertyChanged += this.PersonChanged;
this.Persons = new PersonList();
this.Persons.Add(vm1);
this.Persons.Add(vm2);
this.Persons.Add(vm3);
}
void PersonChanged(object sender, PropertyChangedEventArgs e)
{
this.Persons.OnCollectionChanged();
}
It works, but it's not a clean solution. My next idea would be creating a derivate of ObservableCollection that does the wiring up automatically in a CollectionChanged-handler.
public class SynchronizedObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
foreach (INotifyPropertyChanged item in e.NewItems)
{
item.PropertyChanged += this.ItemChanged;
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
foreach (INotifyPropertyChanged item in e.OldItems)
{
item.PropertyChanged -= this.ItemChanged;
}
break;
}
}
base.OnCollectionChanged(e);
}
void ItemChanged(object sender, PropertyChangedEventArgs e)
{
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
The question is : is there a better way to do this? Is this really necessary?
Thanks a lot in advance for any input!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
不,根本没有必要。您的样品失败的原因很微妙,但很简单。
如果您没有为 WPF 提供数据项模板(例如列表中的
Person
对象),它将默认使用ToString()
方法显示。这是一个成员,而不是一个属性,因此当值更改时您不会收到任何事件通知。如果您将
DisplayMemberPath="Name"
添加到列表框,它将生成一个正确绑定到您的Name
的模板 - 然后该模板将根据您的情况自动更新d 期望。No, it's not necessary at all. The reason your sample is failing is subtle, but quite simple.
If you don't provide WPF with a template for a data item (such as the
Person
objects in your list), it'll default to using theToString()
method to display. That's a member, not a property, and so you get no event notification when the value changes.If you add
DisplayMemberPath="Name"
to your listbox, it'll generate a template that binds properly to theName
of your person - which will then update automatically as you'd expect.我相信这与您对 PersonViewModel 的 ToString() 覆盖有关。如果您删除它,并在 ListBox 上使用 DataTemplate,那么您应该得到预期的行为:
I believe this is to do with your ToString() override on PersonViewModel. If you remove this, and use a DataTemplate on the ListBox instead, then you should get your expected behaviour:
将
DisplayMemberPath="Name"
添加到ListBox
。问题是您依赖ToString()
来显示人名而不是任何属性。这就是为什么提高PropertyChanged
没有任何区别。从现在开始,不要使用方法来评估 Bindings 中的任何值。add
DisplayMemberPath="Name"
toListBox
. The problem is you are relying onToString()
to display person's name and not any property. That is why raisingPropertyChanged
does not make any difference. From now on, don't use a method to evaluate any value in Bindings.