在数据绑定到ComboBox中的CollectionViewSource时,如何保留CurrentItem的TwoWay绑定

假设我们有一个简单的VM类

public class PersonViewModel : Observable { private Person m_Person= new Person("Mike", "Smith"); private readonly ObservableCollection m_AvailablePersons = new ObservableCollection( new List { new Person("Mike", "Smith"), new Person("Jake", "Jackson"), }); public ObservableCollection AvailablePersons { get { return m_AvailablePersons; } } public Person CurrentPerson { get { return m_Person; } set { m_Person = value; NotifyPropertyChanged("CurrentPerson"); } } } 

成功数据绑定到ComboBox就足够了,例如:

  

请注意Person有Equals重载,当我在ViewModel中设置CurrentPerson值时,它会导致combobox当前项显示新值。

现在假设我想使用CollectionViewSource为我的视图添加排序function

         

现在combobox项目源绑定将如下所示:

  

它确实会被排序(如果我们添加更清楚的项目)。

但是,当我们现在更改VM中的CurrentPerson时(在没有CollectionView的情况下使用清除绑定之前,它工作正常),此更改不会显示在绑定的ComboBox中。

我相信在那之后为了从VM设置CurrentItem,我们必须以某种方式访问​​View(我们不要从MVVM中的ViewModel查看View),并调用MoveCurrentTo方法来强制View显示currentItem更改。

因此,通过添加额外的视图function(排序),我们失去了对现有viewModel的TwoWay绑定,我认为这不是预期的行为。

有没有办法在这里保留TwoWay绑定? 或许我做错了。

编辑:实际情况更复杂然后它可能会出现,当我重写CurrentPerson setter像这样:

 set { if (m_AvailablePersons.Contains(value)) { m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First(); } else throw new ArgumentOutOfRangeException("value"); NotifyPropertyChanged("CurrentPerson"); } 

它工作fine

它有缺陷的行为,还是有解释? 由于某些原因,即使Equals重载,它也需要引用 person对象的相等性。

我真的不明白为什么它需要引用相等所以我正在为那些可以解释为什么普通的setter不起作用的人添加赏金 ,当Equal方法重载时可以清楚地看到“修复”使用它的代码

有两个问题联合起来,但你突出了使用CollectionViewSource和ComboBox的一个真正的问题。 我仍在寻找替代方案以“更好的方式”解决这个问题,但是你的setter修复程序有充分的理由避免了这个问题。

我已经完整地再现了你的例子,以确认问题和关于原因的理论。

如果您使用SelectedValue INSTEAD OF SelectedItem,则 ComboBox绑定到CurrentPerson不会使用equals运算符来查找匹配项。 如果你断开你的override bool Equals(object obj)你会看到它在你改变选择时没有被击中。

通过将setter更改为以下内容,您将使用Equals运算符找到特定的匹配对象,因此可以使用2个对象的后续值比较。

 set { if (m_AvailablePersons.Contains(value)) { m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First(); } else throw new ArgumentOutOfRangeException("value"); NotifyPropertyChanged("CurrentPerson"); } 

现在真正有趣的结果:

即使您将代码更改为使用SelectedItem,它也可以正常绑定到列表,但仍然无法绑定到已排序的视图!

我将调试输出添加到Equals方法,即使找到匹配项,它们也会被忽略:

 public override bool Equals(object obj) { if (obj is Person) { Person other = obj as Person; if (other.Firstname == Firstname && other.Surname == Surname) { Debug.WriteLine(string.Format("{0} == {1}", other.ToString(), this.ToString())); return true; } else { Debug.WriteLine(string.Format("{0} <> {1}", other.ToString(), this.ToString())); return false; } } return base.Equals(obj); } 

我的结论……

…是ComboBox在幕后找到匹配项,但由于它与原始数据之间存在CollectionViewSource,因此忽略匹配并比较对象(以决定选择哪一个)。 从内存中,CollectionViewSource管理自己当前选择的项目, 因此如果您没有获得精确的对象匹配,则使用带有ComboxBox的CollectionViewSource将无法工作

基本上你的setter改变是有效的,因为它保证了CollectionViewSource上的对象匹配,然后保证ComboBox上的对象匹配。

测试代码

对于那些想玩的人来说,完整的测试代码如下(对于代码隐藏的黑客感到遗憾,但这只是用于测试而不是MVVM)。

只需创建一个新的Silverlight 4应用程序并添加这些文件/更改:

PersonViewModel.cs

 using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Linq; namespace PersonTests { public class PersonViewModel : INotifyPropertyChanged { private Person m_Person = null; private readonly ObservableCollection m_AvailablePersons = new ObservableCollection(new List { new Person("Mike", "Smith"), new Person("Jake", "Jackson"), new Person("Anne", "Aardvark"), }); public ObservableCollection AvailablePersons { get { return m_AvailablePersons; } } public Person CurrentPerson { get { return m_Person; } set { if (m_Person != value) { m_Person = value; NotifyPropertyChanged("CurrentPerson"); } } //set // This works //{ // if (m_AvailablePersons.Contains(value)) { // m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First(); // } // else throw new ArgumentOutOfRangeException("value"); // NotifyPropertyChanged("CurrentPerson"); //} } private void NotifyPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } public event PropertyChangedEventHandler PropertyChanged; } public class Person { public string Firstname { get; set; } public string Surname { get; set; } public Person(string firstname, string surname) { this.Firstname = firstname; this.Surname = surname; } public override string ToString() { return Firstname + " " + Surname; } public override bool Equals(object obj) { if (obj is Person) { Person other = obj as Person; if (other.Firstname == Firstname && other.Surname == Surname) { Debug.WriteLine(string.Format("{0} == {1}", other.ToString(), this.ToString())); return true; } else { Debug.WriteLine(string.Format("{0} <> {1}", other.ToString(), this.ToString())); return false; } } return base.Equals(obj); } } } 

MainPage.xaml中

                

MainPage.xaml.cs中

 using System.Windows; using System.Windows.Controls; namespace PersonTests { public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); this.DataContext = new PersonViewModel(); } private void button1_Click(object sender, RoutedEventArgs e) { (this.DataContext as PersonViewModel).CurrentPerson = new Person("Mike", "Smith"); } private void button2_Click(object sender, RoutedEventArgs e) { (this.DataContext as PersonViewModel).CurrentPerson = new Person("Anne", "Aardvark"); } } } 

您是否考虑过使用CollectionView并在combobox上设置IsSynchronizedWithCurrentItem?

这就是我要做的 – 而不是拥有你的CurrentPerson属性,你在collectionView.CurrentItem上有选定的人,并且在collectionview上有关于currentitem的combobox。

我已经使用了collectionview进行排序和分组而没有任何问题 – 你可以从中获得与ui的良好解耦。

我会将collectionview移动到代码并在那里绑定它

public ICollectionView AvailablePersonsView {get; private set;}

在ctor:

AvailablePersonsView = CollectionViewSource.GetDefaultView(AvailablePersons)

TwoWay绑定可以正常工作,但是当您从代码中设置SelectedItemSelectedIndex时, ComboBox不会在UI上更新自身。 如果您需要此function,只需扩展ComboBox并侦听从Selectorinheritance的SelectionChanged ,或者如果您只想设置初始选择,请在Loaded

我强烈推荐使用微软的Kyle McClellan的ComboBoxExtensions。

您可以在XAML中为ComboBox声明一个数据源 – 它在异步模式下更加灵活和可用。

基本上,解决方案主要是不要将CollectionViewSource用于ComboBoxes。 您可以在服务器端查询上进行排序。