locked
Bug in WPF combobox data binding RRS feed

  • 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:
            <ComboBox ItemsSource="{Binding Countries, Mode=OneWay}" 
                      SelectedItem="{Binding SelectedCountry2}"  
                      HorizontalAlignment="Left" 
                      VerticalAlignment="Top" 
                      Margin="10" 
                      Width="250" 
                      DisplayMemberPath="Name" 
                      Grid.Row="3" 
                      > 
            </ComboBox> 
    Normally thinking, we set the binding of ItemsSource first, and then set the binding of SelectedItem.
    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.

       
     
    • Proposed as answer by Tao Liang Monday, March 9, 2009 8:45 AM
    • Edited by Tao Liang Monday, March 9, 2009 8:48 AM
    • Marked as answer by Tao Liang Tuesday, March 10, 2009 5:29 AM
    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:
            <ComboBox ItemsSource="{Binding Countries, Mode=OneWay}" 
                      SelectedItem="{Binding SelectedCountry2}"  
                      HorizontalAlignment="Left" 
                      VerticalAlignment="Top" 
                      Margin="10" 
                      Width="250" 
                      DisplayMemberPath="Name" 
                      Grid.Row="3" 
                      > 
            </ComboBox> 
    Normally thinking, we set the binding of ItemsSource first, and then set the binding of SelectedItem.
    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.

       
     
    • Proposed as answer by Tao Liang Monday, March 9, 2009 8:45 AM
    • Edited by Tao Liang Monday, March 9, 2009 8:48 AM
    • Marked as answer by Tao Liang Tuesday, March 10, 2009 5:29 AM
    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