【问题标题】:Binding a WPF ComboBox to a custom list将 WPF ComboBox 绑定到自定义列表
【发布时间】:2010-10-08 08:25:54
【问题描述】:

我有一个似乎没有更新 SelectedItem/SelectedValue 的 ComboBox。

ComboBox ItemsSource 绑定到 ViewModel 类上的属性,该类将一堆 RAS 电话簿条目列为 CollectionView。然后我(在不同的时间)将SelectedItemSelectedValue 绑定到 ViewModel 的另一个属性。我在 save 命令中添加了一个 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 的值,因此 DisplayMemberPathSelectedValuePath 都是 ConnectionViewModel 的 Name 属性。 ComboBox 位于 DataTemplate 中,应用于已将 DataContext 设置为 ViewModel 实例的 Window 上的 ItemsControl

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

我在对 ComboBox 进行数据绑定时缺少什么?我做了很多搜索,似乎找不到任何我做错的地方。


这是我看到的行为,但是在我的特定上下文中由于某种原因它不起作用。

我有一个 MainWindowViewModel,它有一个 CollectionView 的 ConnectionViewModel。在 MainWindowView.xaml 文件代码隐藏中,我将 DataContext 设置为 MainWindowViewModel。 MainWindowView.xaml 有一个ItemsControl 绑定到 ConnectionViewModels 的集合。我有一个包含 ComboBox 以及其他一些 TextBoxes 的 DataTemplate。文本框使用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}" />

TextBox 都正确绑定,数据在它们和 ViewModel 之间移动没有问题。只有 ComboBox 不起作用。

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

如果您运行该示例,您将得到我所说的行为。 TextBox 在您编辑它时会更新其绑定,但 ComboBox 不会。看到我真正做的唯一事情就是引入一个父 ViewModel,这非常令人困惑。

我目前的印象是绑定到 DataContext 的子项的项具有该子项作为其 DataContext。我找不到任何可以以一种或另一种方式解决此问题的文档。

即,

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

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


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

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

这将显示 TextBox 绑定根(我将其与 DataContext 进行比较)是 ConnectionViewModel 实例。

【问题讨论】:

    标签: c# wpf data-binding mvvm combobox


    【解决方案1】:

    您将 DisplayMemberPath 和 SelectedValuePath 设置为“Name”,因此我假设您有一个具有公共属性 Name 的类 PhoneBookEntry。

    您是否将 DataContext 设置为 ConnectionViewModel 对象?

    我复制了你的代码并做了一些小的修改,它似乎工作正常。 我可以设置viewmodels PhoneBookEnty 属性并且组合框中的选定项发生变化,并且我可以更改组合框中的选定项并且视图模型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 的第二个示例似乎不起作用,这对我来说似乎有点奇怪。如果我将 ConnectionViewModel 上的 PhonebookEntries 属性更改为 ReadOnlyCollection 类型,则组合框上 SelectedValue 属性的 TwoWay 绑定可以正常工作。

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

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

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

    【讨论】:

    • 我确实也注意到了这条消息,但我认为覆盖的是基本数据绑定。我猜不会。 :) 我现在将属性公开为 IList&lt;T&gt; 并在属性 getter 中使用 _list.AsReadOnly() 类似于您提到的方式。它的工作原理和我希望原始方法一样。此外,我还想到,虽然 ItemsSource 绑定工作正常,但我可以只使用 ViewModel 中的 Current 属性来访问 ComboBox 中的选定项。尽管如此,它还是不如绑定 ComboBoxes SelectedValue/SelectedItem 属性那么自然。
    • 我可以确认将绑定了ItemsSource 属性的集合更改为只读集合可以使其正常工作。就我而言,我必须将其从 ObservableCollection 更改为 ReadOnlyObservableCollection。坚果。这是 .NET 3.5 - 不确定它是否在 4.0 中修复
    【解决方案2】:

    起初我遇到了同样的问题,但结果证明是由于 NHibernate/WPF 兼容性问题。该问题是由 WPF 检查对象相等性的方式引起的。通过使用 SelectedValue 和 SelectedValuePath 属性中的对象 ID 属性,我能够让我的东西正常工作。

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

    请参阅 Chester 的博文,The WPF ComboBox - SelectedItem, SelectedValue, and SelectedValuePath with NHibernate,了解详细信息。

    【讨论】:

    【解决方案3】:

    将数据绑定到 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 必须是属性,而不是类字段)

    【讨论】:

      【解决方案4】:

      我有一个类似的问题,即 SelectedItem 从未更新。

      我的问题是所选项目与列表中包含的项目不是同一个实例。所以我只需要重写 MyCustomObject 中的 Equals() 方法并比较这两个实例的 ID 以告诉 ComboBox 它是同一个对象。

      public override bool Equals(object obj)
      {
          return this.Id == (obj as MyCustomObject).Id;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多