Привязка WPF ComboBox к пользовательскому списку


183

У меня есть ComboBox, который не обновляет SelectedItem / SelectedValue.

Элемент ComboBox ItemsSource привязан к свойству класса ViewModel, в котором список записей телефонной книги RAS отображается как CollectionView. Затем я связал (в разное время) оба SelectedItemили SelectedValueдругое свойство ViewModel. Я добавил MessageBox в команду сохранения для отладки значений, установленных привязкой данных , но SelectedItem/ SelectedValueпривязка не устанавливается.

Класс ViewModel выглядит примерно так:

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");
        }
    }
}

Коллекция _phonebookEntries инициализируется в конструкторе из бизнес-объекта. ComboBox XAML выглядит примерно так:

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

Меня интересует только фактическое строковое значение, отображаемое в ComboBox, а не любые другие свойства объекта, поскольку это значение, которое мне нужно передать в RAS, когда я хочу установить VPN-соединение, следовательно, DisplayMemberPathи SelectedValuePathоба являются свойством Name ConnectionViewModel. ComboBox DataTemplateприменяется к ItemsControlокну, для которого DataContext был установлен в экземпляр ViewModel.

ComboBox отображает список элементов правильно, и я могу выбрать один в пользовательском интерфейсе без проблем. Однако, когда я отображаю окно сообщения из команды, свойство PhonebookEntry по-прежнему имеет начальное значение, а не выбранное значение из ComboBox. Другие экземпляры TextBox обновляются нормально и отображаются в MessageBox.

Чего мне не хватает в привязке данных ComboBox? Я много искал и не могу найти ничего, что я делаю неправильно.


Это поведение, которое я вижу, однако по какой-то причине оно не работает в моем конкретном контексте.

У меня есть MainWindowViewModel, которая имеет CollectionViewConnectionViewModels. В коде файла MainWindowView.xaml я установил для DataContext значение MainWindowViewModel. MainWindowView.xaml имеет ItemsControlпривязку к коллекции ConnectionViewModels. У меня есть DataTemplate, который содержит ComboBox, а также некоторые другие TextBox. TextBoxes напрямую связаны со свойствами ConnectionViewModel, используя 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; }
}

Код XAML:

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

Тогда 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}" />

Все текстовые поля привязаны правильно, и данные перемещаются между ними и ViewModel без проблем. Только ComboBox не работает.

Вы правы в своем предположении относительно класса PhonebookEntry.

Я предполагаю, что DataContext, используемый моим DataTemplate, автоматически устанавливается через иерархию привязки, поэтому мне не нужно явно устанавливать его для каждого элемента в ItemsControl. Это показалось бы мне немного глупым.


Вот тестовая реализация, которая демонстрирует проблему на основе приведенного выше примера.

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>

Код-за :

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; }
        }
    }
}

Если вы запустите этот пример, вы получите поведение, о котором я говорю. TextBox корректно обновляет свою привязку при редактировании, а ComboBox - нет. Очень запутанно видеть, что единственное, что я сделал, это представил родительскую ViewModel.

В настоящее время у меня складывается впечатление, что элемент, связанный с дочерним элементом DataContext, имеет этот дочерний элемент в качестве своего DataContext. Я не могу найти документацию, которая проясняет это так или иначе.

То есть,

Окно -> DataContext = MainWindowViewModel
..Items -> Связано с DataContext.PhonebookEntries
.... Item -> DataContext = PhonebookEntry (неявно связано)

Я не знаю, объясняет ли это мое предположение лучше (?).


Чтобы подтвердить мое предположение, измените привязку TextBox на

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

И это покажет, что корнем привязки TextBox (который я сравниваю с DataContext) является экземпляр ConnectionViewModel.

Ответы:


189

Вы устанавливаете DisplayMemberPath и SelectedValuePath в «Имя», поэтому я предполагаю, что у вас есть класс PhoneBookEntry с открытым свойством Name.

Вы установили DataContext для вашего объекта ConnectionViewModel?

Я скопировал ваш код и сделал несколько небольших изменений, и, похоже, он работает нормально. Я могу установить свойство Viewmodels PhoneBookEnty и выбранный элемент в выпадающем списке, и я могу изменить выбранный элемент в выпадающем списке, и свойство ViewBook ​​PhoneBookEntry установлено правильно.

Вот мой контент 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>

И вот мой код:

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;
    }
}

Редактировать: второй пример Джеффса, кажется, не работает, что мне кажется немного странным. Если я изменю свойство PhonebookEntries в ConnectionViewModel на тип ReadOnlyCollection , привязка TwoWay свойства SelectedValue в выпадающем списке работает нормально.

Может быть, есть проблема с CollectionView? Я заметил предупреждение в консоли вывода:

System.Windows.Data Предупреждение: 50: Использование CollectionView напрямую не полностью поддерживается. Основные функции работают, хотя с некоторыми недостатками, но расширенные функции могут столкнуться с известными ошибками. Подумайте об использовании производного класса, чтобы избежать этих проблем.

Edit2 (.NET 4.5): содержимое DropDownList может быть основано на ToString (), а не на DisplayMemberPath, тогда как DisplayMemberPath указывает член только для выбранного и отображаемого элемента.


1
Я тоже заметил это сообщение, но предположил, что это будет связано с базовыми данными. Я думаю, нет. :) Теперь я выставляю свойства как IList <T >и в методе получения свойств с помощью _list.AsReadOnly (), аналогично тому, как вы упомянули. Это работает так, как я бы надеялся, что оригинальный метод будет иметь. Кроме того, мне пришло в голову, что, хотя привязка ItemsSource работала нормально, я мог просто использовать свойство Current в ViewModel для доступа к выбранному элементу в ComboBox. Тем не менее, это не так естественно, как привязка свойства ComboBoxes SelectedValue / SelectedItem.
Джефф Беннетт

3
Я могу подтвердить, что изменение коллекции, к которой ItemsSourceпривязано свойство, на коллекцию только для чтения заставляет его работать. В моем случае мне пришлось изменить его с ObservableCollectionна ReadOnlyObservableCollection. Гайки. Это .NET 3.5 - не уверен, что это исправлено в 4.0
ChrisWue

74

Чтобы связать данные с 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";

ComboData выглядит так:

public class ComboData
{ 
  public int Id { get; set; } 
  public string Value { get; set; } 
}

Это решение не работает для меня. ItemsSource работает нормально, но свойства Path неправильно перенаправляют на значения ComboData.
Конус

3
Idи Valueдолжны быть свойства , а не поле класса, например:public class ComboData { public int Id { get; set; } public string Value { get; set; } }
Эдгар

23

У меня была такая проблема, которая сначала казалась идентичной, но это оказалось из-за проблемы совместимости NHibernate / WPF. Проблема была вызвана тем, как WPF проверяет равенство объектов. Я смог заставить свои вещи работать, используя свойство идентификатора объекта в свойствах SelectedValue и SelectedValuePath.

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

Для получения дополнительной информации см. Сообщение в блоге от Chester, ComboBox WPF - SelectedItem, SelectedValue и SelectedValuePath с NHibernate .


1

У меня была похожая проблема, когда SelectedItem никогда не обновлялся.

Моя проблема заключалась в том, что выбранный элемент не совпадает с экземпляром, содержащимся в списке. Поэтому мне просто пришлось переопределить метод Equals () в моем MyCustomObject и сравнить идентификаторы этих двух экземпляров, чтобы сообщить ComboBox, что это один и тот же объект.

public override bool Equals(object obj)
{
    return this.Id == (obj as MyCustomObject).Id;
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.