Answered by:
Bug in WPF combobox data binding
Question
-
Hi I have found a bug in the WPF combobox data binding. When setting the ItemsSource before the SelectedItem in XAML markup it seems that when the datacontext changes and the the ItemSource is bound to another list the SelectedItem data bound object of the previous datacontext is set to null.
<ComboBox ItemsSource="{Binding Countries, Mode=OneWay}" SelectedItem="{Binding SelectedCountry}" DisplayMemberPath="Name" > </ComboBox>
When changing the order of the ItemsSource and the SelectedItem in the markup everything works ok:<ComboBox SelectedItem="{Binding SelectedCountry}" ItemsSource="{Binding Countries, Mode=OneWay}" DisplayMemberPath="Name" > </ComboBox>
This feels a lot more hacky that I would like to ...
I have written a blog post about this in detail and added a little test project that easely reproduces this. Can anybody shine his light on this one ? :)Wednesday, March 4, 2009 12:33 PM
Answers
-
The ComboBox has two bindings that be relative to the change of DataContext:Normally thinking, we set the binding of ItemsSource first, and then set the binding of SelectedItem.
<ComboBox ItemsSource="{Binding Countries, Mode=OneWay}" SelectedItem="{Binding SelectedCountry2}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Width="250" DisplayMemberPath="Name" Grid.Row="3" > </ComboBox>
But there is a side-effect in this binding scenario: the first property ItemsSource will affect the second property SelectedItem. Exactly speaking, when the DataContext changes the bindings are notified in order. So the first property changes the value of the second property before the second binding is notified.
For example, run the demo program (code is below), select Joe in the ListView, and select Belgium in the ComboBox. Now you change the ListView to select Eddy. The first binding reset the ItemSource, which has a side-effect of changing the selection. For the old selected item does not belong to the items collection any more, the ComboBox resets its selection to null. This action put null into Joe’s selectedCountry (Not Eddy) because the second binding has not been notified about the change of DataContext yet. So when you click back to Joe, it selection is null.
If you declare the two bindings in the opposite order, it works as you see.<ComboBox SelectedItem="{Binding SelectedCountry}" ItemsSource="{Binding Countries, Mode=OneWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Width="250" DisplayMemberPath="Name" Grid.Row="1" > </ComboBox>
The SelectItem binding will be notified first, so the SelectedItem binding is using the right DataContext when the side-effect happens.
Monday, March 9, 2009 8:39 AM
All replies
-
Hi Sebastien,
I always set the ItemSource before the SelectedItem and all works fine.Wednesday, March 4, 2009 2:28 PM -
The ComboBox has two bindings that be relative to the change of DataContext:Normally thinking, we set the binding of ItemsSource first, and then set the binding of SelectedItem.
<ComboBox ItemsSource="{Binding Countries, Mode=OneWay}" SelectedItem="{Binding SelectedCountry2}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Width="250" DisplayMemberPath="Name" Grid.Row="3" > </ComboBox>
But there is a side-effect in this binding scenario: the first property ItemsSource will affect the second property SelectedItem. Exactly speaking, when the DataContext changes the bindings are notified in order. So the first property changes the value of the second property before the second binding is notified.
For example, run the demo program (code is below), select Joe in the ListView, and select Belgium in the ComboBox. Now you change the ListView to select Eddy. The first binding reset the ItemSource, which has a side-effect of changing the selection. For the old selected item does not belong to the items collection any more, the ComboBox resets its selection to null. This action put null into Joe’s selectedCountry (Not Eddy) because the second binding has not been notified about the change of DataContext yet. So when you click back to Joe, it selection is null.
If you declare the two bindings in the opposite order, it works as you see.<ComboBox SelectedItem="{Binding SelectedCountry}" ItemsSource="{Binding Countries, Mode=OneWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Width="250" DisplayMemberPath="Name" Grid.Row="1" > </ComboBox>
The SelectItem binding will be notified first, so the SelectedItem binding is using the right DataContext when the side-effect happens.
Monday, March 9, 2009 8:39 AM -
Test Code
XAML:<Window x:Class="TestBindingError.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:TestBindingError="clr-namespace:TestBindingError" Title="Window1" Height="307" Width="681"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Text="Select a person in the list, set a value in the two combo boxes, switch to another person in the list and then switch back to the previous one." Margin="5" TextWrapping="WrapWithOverflow"/> <ListView ItemsSource="{Binding Persons}" SelectedItem="{Binding SelectedPerson}" Height="91" Grid.Row="1" VerticalAlignment="Top"> <ListView.ItemTemplate> <DataTemplate DataType="{x:Type TestBindingError:PersonViewModel}" > <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> <Grid Grid.Row="2" > <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock Text="SelectedItem before ItemsSource in markup, no bug" Margin="5"/> <ComboBox DataContext="{Binding SelectedPerson, Mode=OneWay}" SelectedItem="{Binding SelectedCountry}" ItemsSource="{Binding Countries, Mode=OneWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Width="250" DisplayMemberPath="Name" Grid.Row="1" > </ComboBox> <TextBlock Text="SelectedItem after ItemsSource in markup, bug" Grid.Row="2" Margin="5"/> <ComboBox DataContext="{Binding SelectedPerson, Mode=OneWay}" ItemsSource="{Binding Countries, Mode=OneWay}" SelectedItem="{Binding SelectedCountry2}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Width="250" DisplayMemberPath="Name" Grid.Row="3" > </ComboBox> </Grid> </Grid> </Window>
C#:using System.Collections.ObjectModel; using System.Windows; using System.ComponentModel; namespace TestBindingError { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); var appViewModel = new AppViewModel { Persons = new ObservableCollection<PersonViewModel> { new PersonViewModel {Name = "Joe"}, new PersonViewModel {Name = "Eddy"}, new PersonViewModel {Name = "Francois"} } }; DataContext = appViewModel; } } public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } public class AppViewModel : ViewModelBase { private ObservableCollection<PersonViewModel> persons; private PersonViewModel person; public ObservableCollection<PersonViewModel> Persons { get { return persons; } set { persons = value; OnPropertyChanged("Persons"); } } public PersonViewModel SelectedPerson { get { return person; } set { person = value; OnPropertyChanged("SelectedPerson"); } } } public class Country { public string Code { get; set; } public string Name { get; set; } } public class PersonViewModel : ViewModelBase { private string name; private ObservableCollection<Country> countries; private Country selectedCountry; private Country selectedCountry2; public string Name { get { return name; } set { name = value; OnPropertyChanged("Name"); } } public ObservableCollection<Country> Countries { get { if (countries == null) { countries = new ObservableCollection<Country> { new Country {Code = "BE", Name = "Belgium"}, new Country {Code = "NL", Name = "Netherlands"}, new Country {Code = "US", Name = "United States"}, new Country {Code = "FR", Name = "France"} }; } return countries; } set { countries = value; OnPropertyChanged("Countries"); } } public Country SelectedCountry { get { return selectedCountry; } set { selectedCountry = value; OnPropertyChanged("SelectedCountry"); } } public Country SelectedCountry2 { get { return selectedCountry2; } set { selectedCountry2 = value; OnPropertyChanged("SelectedCountry2"); } } } }
Monday, March 9, 2009 8:44 AM -
Hi liantom,
Thanks for your answer. But still I find it a bit strange and counter intuitive that if you the change the datacontext of a WPF control a sub element still can hold a refence to the previous datacontext.
seba
Monday, March 9, 2009 8:55 AM -
You can debug it.
For example:
XAML:
<Window x:Class="TestBindingError.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:TestBindingError="clr-namespace:TestBindingError" Title="Window1" Height="307" Width="681"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Text="Select a person in the list, set a value in the two combo boxes, switch to another person in the list and then switch back to the previous one." Margin="5" TextWrapping="WrapWithOverflow"/> <ListView ItemsSource="{Binding Persons}" SelectedItem="{Binding SelectedPerson}" Height="91" Grid.Row="1" VerticalAlignment="Top"> <ListView.ItemTemplate> <DataTemplate DataType="{x:Type TestBindingError:PersonViewModel}" > <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> <Grid Grid.Row="2" > <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock Text="SelectedItem before ItemsSource in markup, no bug" Margin="5"/> <ComboBox DataContext="{Binding SelectedPerson, Mode=OneWay}" SelectedItem="{Binding SelectedCountry}" ItemsSource="{Binding Countries, Mode=OneWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Width="250" DisplayMemberPath="Name" Grid.Row="1" SelectionChanged="ComboBox_SelectionChanged"> </ComboBox> <!--<TextBlock Text="SelectedItem after ItemsSource in markup, bug" Grid.Row="2" Margin="5"/> <ComboBox DataContext="{Binding SelectedPerson, Mode=OneWay}" ItemsSource="{Binding Countries, Mode=OneWay}" SelectedItem="{Binding SelectedCountry2}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Width="250" DisplayMemberPath="Name" Grid.Row="3"> </ComboBox>--> </Grid> </Grid> </Window>
C#:
using System.Collections.ObjectModel; using System.Windows; using System.ComponentModel; using System; namespace TestBindingError { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public AppViewModel appViewModel = null; public Window1() { InitializeComponent(); appViewModel = new AppViewModel { Persons = new ObservableCollection<PersonViewModel> { new PersonViewModel {Name = "Joe"}, new PersonViewModel {Name = "Eddy"}, new PersonViewModel {Name = "Francois"} } }; DataContext = appViewModel; } private void ComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { Display( ); } public void Display() { for (int i = 0; i < appViewModel.Persons.Count; i++) { PersonViewModel pvm = appViewModel.Persons[i]; string name = pvm.Name; string select1 = "null"; if (pvm.SelectedCountry != null) { select1 = pvm.SelectedCountry.Name; } Console.WriteLine( name + "'s country1: "+ select1 ); } Console.WriteLine("**********************************"); } } public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } public class AppViewModel : ViewModelBase { private ObservableCollection<PersonViewModel> persons; private PersonViewModel person; public ObservableCollection<PersonViewModel> Persons { get { return persons; } set { persons = value; OnPropertyChanged("Persons"); } } public PersonViewModel SelectedPerson { get { return person; } set { person = value; OnPropertyChanged("SelectedPerson"); } } } public class Country { public string Code { get; set; } public string Name { get; set; } } public class PersonViewModel : ViewModelBase { private string name; private ObservableCollection<Country> countries; private Country selectedCountry; private Country selectedCountry2; public string Name { get { return name; } set { name = value; OnPropertyChanged("Name"); } } public ObservableCollection<Country> Countries { get { if (countries == null) { countries = new ObservableCollection<Country> { new Country {Code = "BE", Name = "Belgium"}, new Country {Code = "NL", Name = "Netherlands"}, new Country {Code = "US", Name = "United States"}, new Country {Code = "FR", Name = "France"} }; } return countries; } set { countries = value; OnPropertyChanged("Countries"); } } public Country SelectedCountry { get { return selectedCountry; } set { selectedCountry = value; OnPropertyChanged("SelectedCountry"); } } public Country SelectedCountry2 { get { return selectedCountry2; } set { selectedCountry2 = value; OnPropertyChanged("SelectedCountry2"); } } } } Monday, March 9, 2009 9:32 AM