Posting CollectionChanged and PropertyChanged (or: why are some WPF bindings not updated?)

WPF DataBindings is used to make me happy. One thing that I just stumbled upon right now is that at some point they just don't get updated as intended. 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> 

Code for 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 are 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 would like: When I select an entry from a ListBox and change its name in a TextBox, the list is updated to display the new value.

What happens: nothing. And this is the right behavior if I am a judge. I made sure that the SelectedItem PropertyChanged was fired, but that (of course) does not cause CollectionChanged to start.

To fix this, I created a class based on ObservableCollection, which 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 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 is not a clean solution. My next idea would be to create a Derivative ObservableCollection that automatically sets the wiring in the 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)); } } 

Question: is there a better way to do this? Is it really necessary?

Thanks so much for any input!

+1
source share
3 answers

No, this is not at all necessary. The reason your sample fails is subtle, but rather simple.

If WPF does not provide a template for the data item (for example, the Person objects in your list), the ToString() method will be used by default for display. This is a member, not a property, and therefore you do not receive event notifications when a value changes.

If you add DisplayMemberPath="Name" to your list, it will generate a template that correctly binds to the Name your face - which will then automatically update as you expected.

+4
source

I believe this is due to overriding ToString () in PersonViewModel. If you delete this and use the DataTemplate in the ListBox instead, you should get the expected behavior:

 <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.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Name}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Path=SelectedPerson.Name, UpdateSourceTrigger=PropertyChanged}"/> </Grid> 

+1
source

add DisplayMemberPath="Name" to the ListBox . The problem is that you rely on ToString() to display the name of the person, not some property. That's why raising a PropertyChanged doesn't make any difference. From now on, do not use the method to evaluate any value in Bindings.

+1
source

Source: https://habr.com/ru/post/988736/


All Articles