假设我们有一个简单的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/

10-12 16:11
查看更多