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 {
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!