Связывание WPF ComboBox с пользовательским списком
У меня есть ComboBox, который, похоже, не обновляет SelectedItem/SelectedValue.
Компонент ComboBox ItemsSource привязан к свойству класса ViewModel, в котором перечислены записи записей телефонной книги RAS в виде CollectionView, после чего я связал (в разное время) как SelectedItem
или SelectedValue
с другим свойством ViewModel. Я добавил MessageBox в команду save для отладки значений, заданных привязкой данных, но привязка 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, который имеет CollectionView
ConnectionViewModels. В исходном коде MainWindowView.xaml я установил DataContext в MainWindowViewModel. MainWindowView.xaml имеет ItemsControl
, привязанный к коллекции ConnectionViewModels. У меня есть DataTemplate, который содержит ComboBox, а также некоторые другие 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}" />
TextBoxes все правильно связывают, и данные перемещаются между ними и 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. Я не могу найти документацию, которая очищает это так или иначе.
I.e,
Окно → DataContext = MainWindowViewModel
..Items → Связано с DataContext.PhonebookEntries
.... Item → DataContext = PhonebookEntry (неявно связанный)
Я не знаю, объясняет ли это мое предположение лучше?
Чтобы подтвердить мое предположение, измените привязку TextBox к
<TextBox Text="{Binding Mode=OneWay}" Width="50" />
И это покажет, что корень привязки TextBox (который я сравниваю с DataContext) является экземпляром ConnectionViewModel.
Ответы
Ответ 1
Вы устанавливаете DisplayMemberPath и SelectedValuePath в "Name", поэтому я предполагаю, что у вас есть класс PhoneBookEntry с публичным именем свойства.
Установили ли DataContext объект ConnectionViewModel?
Я скопировал ваш код и внес некоторые незначительные изменения, и, похоже, он работает нормально.
Я могу установить свойства viewModels PhoneBookEnty и выбранный элемент в изменениях со списком, и я могу изменить выбранный элемент в свойствах combobox и view, свойство 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;
}
}
Изменить: Второй пример Geoffs, похоже, не работает, что кажется мне немного странным. Если я изменить свойство PhonebookEntries в ConnectionViewModel на тип ReadOnlyCollection, привязка TwoWay свойства SelectedValue в combobox отлично работает.
Может быть, проблема с CollectionView? Я заметил предупреждение о выпуске консоли:
"System.Windows.Data Warning: 50: Использование CollectionView напрямую не поддерживается полностью. Основные функции работают, хотя и с некоторыми неэффективными, но расширенные функции могут сталкиваться с известными ошибками. Подумайте об использовании производного класса, чтобы избежать этих проблем".
Edit2 (.Net 4.5): Содержимое DropDownList может быть основано на ToString(), а не на DisplayMemberPath, а DisplayMemberPath указывает член только для выбранного и отображаемого элемента.
Ответ 2
Чтобы привязать данные к 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; }
}
Ответ 3
У меня было то, что сначала казалось одинаковой проблемой, но оказалось, что это связано с проблемой совместимости с NHibernate/WPF. Проблема была вызвана тем, как WPF проверяет равенство объектов. Мне удалось заставить мои вещи работать, используя свойство ID объекта в свойствах SelectedValue и SelectedValuePath.
<ComboBox Name="CategoryList"
DisplayMemberPath="CategoryName"
SelectedItem="{Binding Path=CategoryParent}"
SelectedValue="{Binding Path=CategoryParent.ID}"
SelectedValuePath="ID">
См. следующую ссылку от Честера:
WPF ComboBox - SelectedItem, SelectedValue и SelectedValuePath с NHibernate
Ответ 4
У меня была аналогичная проблема, когда SelectedItem никогда не обновлялся.
Моя проблема заключалась в том, что выбранный элемент был не тем же экземпляром, что и элемент, содержащийся в списке. Поэтому мне просто пришлось переопределить метод Equals() в MyCustomObject и сравнить идентификатор этих двух экземпляров, чтобы сообщить ComboBox, что это тот же объект.
public override bool Equals(object obj)
{
return this.Id == (obj as MyCustomObject).Id;
}