views:

43748

answers:

7

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 who's 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.

+17  A: 

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:

<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 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 the the ouput 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."

Kjetil Watnedal
+1  A: 

Hi Kjetil.

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.

Geoff Bennett
A: 

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 it's 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?

Geoff Bennett
+1  A: 

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.

Geoff Bennett
I've updated my answere above.
Kjetil Watnedal
A: 

I did notice that message as well, but I assumed what was covered would've been basic data binding. I guess not. :)

I'm now exposing the properties as IList<T> and in the property getter using _list.AsReadOnly() similar to the way you mentioned. It's working as I would've hoped the original method would have.

Also, it crossed my mind that while the ItemsSource binding was working fine, I could've just used the Current property in the ViewModel to access the selected item in the ComboBox. Still, it doesn't feel as natural as binding the ComboBoxes SelectedValue/SelectedItem property.

Thanks heaps for the effort you put into helping me. It has given me a solution I can work with. Points to you. :)

Geoff Bennett
+3  A: 

I had what at first seemed to be an identical problem, but it turned out to be due to an NHibernate/WPF compatability issue. The problem was caused by the way WPF checks for object equality. I was able to get my stuff to work by using the object ID property in the SelectedValue and SelectedValuePath properties.

<ComboBox Name="CategoryList" 
DisplayMemberPath="CategoryName" 
SelectedItem="{Binding Path=CategoryParent}" 
SelectedValue="{Binding Path=CategoryParent.ID}"
SelectedValuePath="ID">

See the following link from Chester for details: The WPF ComboBox - SelectedItem, SelectedValue, and SelectedValuePath with NHibernate

CyberMonk
+3  A: 

Hi Lee See this .. To bind the data to ComboBox

        List<ComboData> ListData = new List<ComboData>();
        ListData.Add(new ComboData { Id = "1", Value = "One" });
        ListData.Add(new ComboData { Id = "2", Value = "Two" });
        ListData.Add(new ComboData { Id = "3", Value = "Three" });
        ListData.Add(new ComboData { Id = "4", Value = "Four" });
        ListData.Add(new ComboData { Id = "5", Value = "Five" });

        cbotest.ItemsSource = ListData;
        cbotest.DisplayMemberPath = "Value";
        cbotest.SelectedValuePath = "Id";

        cbotest.SelectedValue = "2";
Roy