Binding a WPF ComboBox to a custom list

Geoff Bennett picture Geoff Bennett · Feb 18, 2009 · Viewed 583.5k times · Source

I have a ComboBox that doesn't seem to update the SelectedItem/SelectedValue.

The ComboBox ItemsSource is bound to a property on a ViewModel class that lists a bunch of RAS phonebook entries as a CollectionView. Then I've bound (at separate times) both the SelectedItem or SelectedValue to another property of the ViewModel. I have added a MessageBox into the save command to debug the values set by the databinding, but the SelectedItem/SelectedValue binding is not being set.

The ViewModel class looks something like this:

public ConnectionViewModel
{
    private readonly CollectionView _phonebookEntries;
    private string _phonebookeEntry;

    public CollectionView PhonebookEntries
    {
        get { return _phonebookEntries; }
    }

    public string PhonebookEntry
    {
        get { return _phonebookEntry; }
        set
        {
            if (_phonebookEntry == value) return;
            _phonebookEntry = value;
            OnPropertyChanged("PhonebookEntry");
        }
    }
}

The _phonebookEntries collection is being initialised in the constructor from a business object. The ComboBox XAML looks something like this:

<ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
    DisplayMemberPath="Name"
    SelectedValuePath="Name"
    SelectedValue="{Binding Path=PhonebookEntry}" />

I am only interested in the actual string value displayed in the ComboBox, not any other properties of the object as this is the value I need to pass across to RAS when I want to make the VPN connection, hence DisplayMemberPath and SelectedValuePath are both the Name property of the ConnectionViewModel. The ComboBox is in a DataTemplate applied to an ItemsControl on a Window whose DataContext has been set to a ViewModel instance.

The ComboBox displays the list of items correctly, and I can select one in the UI with no problem. However when I display the message box from the command, the PhonebookEntry property still has the initial value in it, not the selected value from the ComboBox. Other TextBox instances are updating fine and displaying in the MessageBox.

What am I missing with databinding the ComboBox? I've done a lot of searching and can't seem to find anything that I'm doing wrong.


This is the behaviour I'm seeing, however it's not working for some reason in my particular context.

I have a MainWindowViewModel which has a CollectionView of ConnectionViewModels. In the MainWindowView.xaml file code-behind, I set the DataContext to the MainWindowViewModel. The MainWindowView.xaml has an ItemsControl bound to the collection of ConnectionViewModels. I have a DataTemplate that holds the ComboBox as well as some other TextBoxes. The TextBoxes are bound directly to properties of the ConnectionViewModel using Text="{Binding Path=ConnectionName}".

public class ConnectionViewModel : ViewModelBase
{
    public string Name { get; set; }
    public string Password { get; set; }
}

public class MainWindowViewModel : ViewModelBase
{
    // List<ConnectionViewModel>...
    public CollectionView Connections { get; set; }
}

The XAML code-behind:

public partial class Window1
{
    public Window1()
    {
        InitializeComponent();
        DataContext = new MainWindowViewModel();
    }
}

Then XAML:

<DataTemplate x:Key="listTemplate">
    <Grid>
        <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
            DisplayMemberPath="Name"
            SelectedValuePath="Name"
            SelectedValue="{Binding Path=PhonebookEntry}" />
        <TextBox Text="{Binding Path=Password}" />
    </Grid>
</DataTemplate>

<ItemsControl ItemsSource="{Binding Path=Connections}"
    ItemTemplate="{StaticResource listTemplate}" />

The TextBoxes all bind correctly, and data moves between them and the ViewModel with no trouble. It's only the ComboBox that isn't working.

You are correct in your assumption regarding the PhonebookEntry class.

The assumption I am making is that the DataContext used by my DataTemplate is automatically set through the binding hierarchy, so that I don't have to explicitly set it for each item in the ItemsControl. That would seem a bit silly to me.


Here is a test implementation that demonstrates the problem, based on the example above.

XAML:

<Window x:Class="WpfApplication7.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <DataTemplate x:Key="itemTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBox Text="{Binding Path=Name}" Width="50" />
                <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
                    DisplayMemberPath="Name"
                    SelectedValuePath="Name"
                    SelectedValue="{Binding Path=PhonebookEntry}"
                    Width="200"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ItemsControl ItemsSource="{Binding Path=Connections}"
            ItemTemplate="{StaticResource itemTemplate}" />
    </Grid>
</Window>

The code-behind:

namespace WpfApplication7
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel();
        }
    }

    public class PhoneBookEntry
    {
        public string Name { get; set; }
        public PhoneBookEntry(string name)
        {
            Name = name;
        }
    }

    public class ConnectionViewModel : INotifyPropertyChanged
    {

        private string _name;

        public ConnectionViewModel(string name)
        {
            _name = name;
            IList<PhoneBookEntry> list = new List<PhoneBookEntry>
                                             {
                                                 new PhoneBookEntry("test"),
                                                 new PhoneBookEntry("test2")
                                             };
            _phonebookEntries = new CollectionView(list);
        }
        private readonly CollectionView _phonebookEntries;
        private string _phonebookEntry;

        public CollectionView PhonebookEntries
        {
            get { return _phonebookEntries; }
        }

        public string PhonebookEntry
        {
            get { return _phonebookEntry; }
            set
            {
                if (_phonebookEntry == value) return;
                _phonebookEntry = value;
                OnPropertyChanged("PhonebookEntry");
            }
        }

        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value) return;
                _name = value;
                OnPropertyChanged("Name");
            }
        }
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class MainWindowViewModel
    {
        private readonly CollectionView _connections;

        public MainWindowViewModel()
        {
            IList<ConnectionViewModel> connections = new List<ConnectionViewModel>
                                                          {
                                                              new ConnectionViewModel("First"),
                                                              new ConnectionViewModel("Second"),
                                                              new ConnectionViewModel("Third")
                                                          };
            _connections = new CollectionView(connections);
        }

        public CollectionView Connections
        {
            get { return _connections; }
        }
    }
}

If you run that example, you will get the behaviour I'm talking about. The TextBox updates its binding fine when you edit it, but the ComboBox does not. Very confusing seeing as really the only thing I've done is introduce a parent ViewModel.

I am currently labouring under the impression that an item bound to the child of a DataContext has that child as its DataContext. I can't find any documentation that clears this up one way or the other.

I.e.,

Window -> DataContext = MainWindowViewModel
..Items -> Bound to DataContext.PhonebookEntries
....Item -> DataContext = PhonebookEntry (implicitly associated)

I don't know if that explains my assumption any better(?).


To confirm my assumption, change the binding of the TextBox to be

<TextBox Text="{Binding Mode=OneWay}" Width="50" />

And this will show the TextBox binding root (which I'm comparing to the DataContext) is the ConnectionViewModel instance.

Answer

Kjetil Watnedal picture Kjetil Watnedal · Feb 18, 2009

You set the DisplayMemberPath and the SelectedValuePath to "Name", so I assume that you have a class PhoneBookEntry with a public property Name.

Have you set the DataContext to your ConnectionViewModel object?

I copied you code and made some minor modifications, and it seems to work fine. I can set the viewmodels PhoneBookEnty property and the selected item in the combobox changes, and I can change the selected item in the combobox and the view models PhoneBookEntry property is set correctly.

Here is my XAML content:

<Window x:Class="WpfApplication6.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
<Grid>
    <StackPanel>
        <Button Click="Button_Click">asdf</Button>
        <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
                  DisplayMemberPath="Name"
                  SelectedValuePath="Name"
                  SelectedValue="{Binding Path=PhonebookEntry}" />
    </StackPanel>
</Grid>
</Window>

And here is my code-behind:

namespace WpfApplication6
{

    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            ConnectionViewModel vm = new ConnectionViewModel();
            DataContext = vm;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            ((ConnectionViewModel)DataContext).PhonebookEntry = "test";
        }
    }

    public class PhoneBookEntry
    {
        public string Name { get; set; }

        public PhoneBookEntry(string name)
        {
            Name = name;
        }

        public override string ToString()
        {
            return Name;
        }
    }

    public class ConnectionViewModel : INotifyPropertyChanged
    {
        public ConnectionViewModel()
        {
            IList<PhoneBookEntry> list = new List<PhoneBookEntry>();
            list.Add(new PhoneBookEntry("test"));
            list.Add(new PhoneBookEntry("test2"));
            _phonebookEntries = new CollectionView(list);
        }

        private readonly CollectionView _phonebookEntries;
        private string _phonebookEntry;

        public CollectionView PhonebookEntries
        {
            get { return _phonebookEntries; }
        }

        public string PhonebookEntry
        {
            get { return _phonebookEntry; }
            set
            {
                if (_phonebookEntry == value) return;
                _phonebookEntry = value;
                OnPropertyChanged("PhonebookEntry");
            }
        }

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Edit: Geoffs second example does not seem to work, which seems a bit odd to me. If I change the PhonebookEntries property on the ConnectionViewModel to be of type ReadOnlyCollection, the TwoWay binding of the SelectedValue property on the combobox works fine.

Maybe there is an issue with the CollectionView? I noticed a warning in the output console:

System.Windows.Data Warning: 50 : Using CollectionView directly is not fully supported. The basic features work, although with some inefficiencies, but advanced features may encounter known bugs. Consider using a derived class to avoid these problems.

Edit2 (.NET 4.5): The content of the DropDownList can be based on ToString() and not of DisplayMemberPath, while DisplayMemberPath specifies the member for the selected and displayed item only.