假设我们有一个简单的VM类
public class PersonViewModel : Observable
{
private Person m_Person= new Person("Mike", "Smith");
private readonly ObservableCollection<Person> m_AvailablePersons =
new ObservableCollection<Person>( new List<Person> {
new Person("Mike", "Smith"),
new Person("Jake", "Jackson"),
});
public ObservableCollection<Person> AvailablePersons
{
get { return m_AvailablePersons; }
}
public Person CurrentPerson
{
get { return m_Person; }
set
{
m_Person = value;
NotifyPropertyChanged("CurrentPerson");
}
}
}
例如,成功将数据成功绑定(bind)到ComboBox就足够了:
<ComboBox ItemsSource="{Binding AvailablePersons}"
SelectedValue="{Binding Path=CurrentPerson, Mode=TwoWay}" />
请注意,Person重载了
Equals
,当我在ViewModel中设置CurrentPerson值时,它将导致组合框当前项显示新值。现在说我想使用
CollectionViewSource
向我的 View 添加排序功能 <UserControl.Resources>
<CollectionViewSource x:Key="PersonsViewSource" Source="{Binding AvailablePersons}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Surname" Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</UserControl.Resources>
现在,组合框项目的源绑定(bind)将如下所示:
<ComboBox ItemsSource="{Binding Source={StaticResource PersonsViewSource}}"
SelectedValue="{Binding Path=CurrentPerson, Mode=TwoWay}" />
并且确实会进行排序(如果我们添加更多其清晰可见的项目)。
但是,当我们现在在VM中更改
CurrentPerson
时(在没有CollectionView的情况下进行清晰绑定(bind)之前,它可以正常工作),此更改不会显示在绑定(bind)的ComboBox中。我相信,在那之后,为了从VM设置CurrentItem,我们必须以某种方式访问View(并且不要在MVVM中从ViewModel转到View),并调用
MoveCurrentTo
方法强制View显示currentItem更改。因此,通过添加其他 View 功能(排序),我们失去了与现有viewModel的TwoWay绑定(bind),我认为这不是预期的行为。
有没有办法在这里保留TwoWay绑定(bind)?也许我做错了。
编辑:当我这样重写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");
}
它的 buggy 性能,还是有解释?由于某些原因,即使
Equals
重载,它也需要对person对象使用引用相等性。我真的不明白为什么它需要引用相等,因此我为有人添加了赏金,可以解释为什么当
Equal
方法重载时正常的setter无法正常工作,可以在使用它的“修复”代码中清楚地看到 最佳答案
有两个问题困扰着您,但是您已经强调了将CollectionViewSource与ComboBox一起使用时的实际问题。我仍在寻找以“更好的方式”解决此问题的替代方法,但是您的二传手修复程序有充分的理由避免了该问题。
我已详细复制了您的示例,以确认问题和有关原因的理论。
如果您使用SelectedValue INSTEAD OF SelectedItem ,则与CurrentPerson绑定(bind)的ComboBox不会使用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,它也可以正常绑定(bind)到列表,但仍然无法绑定(bind)到排序 View !
我将调试输出添加到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<Person> m_AvailablePersons =
new ObservableCollection<Person>(new List<Person> {
new Person("Mike", "Smith"),
new Person("Jake", "Jackson"),
new Person("Anne", "Aardvark"),
});
public ObservableCollection<Person> 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
<UserControl x:Class="PersonTests.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:scm="clr-namespace:System.ComponentModel;assembly=System.Windows" mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<CollectionViewSource x:Key="PersonsViewSource" Source="{Binding AvailablePersons}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Surname" Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</UserControl.Resources>
<StackPanel x:Name="LayoutRoot" Background="LightBlue" Width="150">
<!--<ComboBox ItemsSource="{Binding AvailablePersons}"
SelectedItem="{Binding Path=CurrentPerson, Mode=TwoWay}" />-->
<ComboBox ItemsSource="{Binding Source={StaticResource PersonsViewSource}}"
SelectedItem="{Binding Path=CurrentPerson, Mode=TwoWay}" />
<Button Content="Select Mike Smith" Height="23" Name="button1" Click="button1_Click" />
<Button Content="Select Anne Aardvark" Height="23" Name="button2" Click="button2_Click" />
</StackPanel>
</UserControl>
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");
}
}
}
关于c# - 在ComboBox中将数据绑定(bind)到CollectionViewSource时如何保留CurrentItem的TwoWay绑定(bind),我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/6305608/