将WPF ComboBox绑定到自定义列表

207

我有一个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中显示的实际字符串值感兴趣,不关心对象的其他属性,因为这是我需要在想要建立VPN连接时传递给RAS的值,因此DisplayMemberPath和SelectedValuePath都是ConnectionViewModel的Name属性。该ComboBox位于应用于窗口上ItemsControl的DataTemplate中,DataContext已设置为ViewModel实例。

ComboBox正确显示了列表项,并且我可以在UI中选择其中一个,但是当我从命令显示消息框时,PhonebookEntry属性仍然具有初始值而不是ComboBox中选定的值。其他TextBox实例更新得很好并显示在MessageBox中。

我在数据绑定ComboBox方面错过了什么?我已经搜索了很多,但似乎找不到我的错误。


这就是我看到的行为,但出于某些原因,在我特定的环境中它无法正常工作。

我有一个MainWindowViewModel,其中有一个ConnectionViewModels的CollectionView。在MainWindowView.xaml文件的代码后台中,我将DataContext设置为MainWindowViewModel。MainWindowView.xaml绑定到ConnectionViewModels集合的ItemsControl。我有一个DataTemplate,其中包含ComboBox以及其他一些TextBox。TextBox直接使用Binding Text ="{Binding Path = ConnectionName}"将其绑定到ConnectionViewModel的属性。

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之间流动得很顺畅。唯独下拉列表框没有工作。

关于PhonebookEntry类,你的假设是正确的。

我的假设是,使用DataTemplate的DataContext会通过绑定层次结构自动设置,因此我不必为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; }
        }
    }
}

如果您运行该示例,您将得到我所说的行为。当您编辑文本框时,它会更新其绑定,但组合框不会。非常令人困惑,因为我唯一做的事情就是引入一个父ViewModel。

目前我认为,绑定到DataContext的子项具有该子项作为其DataContext。我找不到任何可以澄清这个问题的文档。

即,

窗口 -> DataContext = MainWindowViewModel
..项 -> 绑定到DataContext.PhonebookEntries
....项 -> DataContext = PhonebookEntry(隐式关联)

我不知道这是否更好地解释了我的假设(?)。


为了确认我的假设,请将TextBox的绑定更改为

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

这将显示文本框绑定根(我正在将其与DataContext进行比较)是ConnectionViewModel实例。

4个回答

220

你设置了DisplayMemberPath和SelectedValuePath为"Name",所以我假设你有一个名为PhoneBookEntry的类,并且有一个公共属性Name。

你是否已将DataContext设置为你的ConnectionViewModel对象?

我复制了你的代码并进行了一些小修改,看起来运行良好。 我可以设置viewmodels PhoneBookEntry属性,combobox中选定的项会发生变化,我也可以更改combobox中选定的项,view models 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;
    }
}

编辑:Geoff的第二个示例似乎不起作用,这对我来说有点奇怪。如果我将ConnectionViewModel上的PhonebookEntries属性更改为ReadOnlyCollection类型,则组合框上SelectedValue属性的双向绑定就能正常工作了。

也许CollectionView存在问题?我在输出控制台中注意到了一个警告:

System.Windows.Data Warning: 50 : 直接使用CollectionView不被完全支持。基本功能可以工作,但某些高级功能可能会遇到已知的错误。考虑使用派生类来避免这些问题。

编辑2(.NET 4.5):DropDownList的内容可以基于ToString()而不是DisplayMemberPath,而DisplayMemberPath仅指定所选和显示项目的成员。


1
我也注意到了那个消息,但我认为被覆盖的内容应该是基本数据绑定。看来不是这样。 :) 我现在将属性公开为IList<T>,并在属性getter中使用_list.AsReadOnly(),类似于您提到的方式。它的工作方式就像我最初希望的那样。此外,我想到了虽然ItemsSource绑定正常工作,但我可以只使用ViewModel中的Current属性来访问ComboBox中选择的项目。尽管如此,它仍然不像绑定ComboBoxes SelectedValue/SelectedItem属性那样自然。 - Geoff Bennett
3
我可以确认,将ItemsSource属性绑定的集合更改为只读集合就可以使其正常工作。在我的情况下,我不得不将它从ObservableCollection更改为ReadOnlyObservableCollection。真是让人头疼。这是.NET 3.5版本 - 不确定它是否在4.0版本中修复了。 - ChrisWue

90
将数据绑定到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; } 
}

(请注意,IdValue必须是属性,而不是类字段)


28

起初我遇到了一个看似相同的问题,但后来发现这是由于NHibernate/WPF兼容性问题引起的。问题是由WPF检查对象平等性的方式所引起的。通过在SelectedValue和SelectedValuePath属性中使用对象ID属性,我成功解决了我的问题。

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

请参考 Chester 的博客文章:使用 NHibernate 处理 WPF ComboBox 的 SelectedItem、SelectedValue 和 SelectedValuePath,获取更多详细信息。


链接已失效,编辑队列已满,因此我将在此处放置当前链接:http://www.skimedic.com/blog/post/2008/10/02/The-WPF-ComboBox-SelectedItem-SelectedValue-and-SelectedValuePath-with-NHibernate - natiiix
很遗憾,如果ComboBox位于DataTemplate中,这种方法是行不通的。为什么呢?因为它涉及到XAML。 - undefined

1
我曾遇到类似的问题,即 SelectedItem 永远不会更新。
我的问题在于所选项目与列表中包含的项目不是同一实例。因此,我只需在 MyCustomObject 中重写 Equals() 方法,并比较这两个实例的 ID,以告诉 ComboBox 它们是相同的对象。
public override bool Equals(object obj)
{
    return this.Id == (obj as MyCustomObject).Id;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接